1{-# LANGUAGE UndecidableInstances #-}
2{-# OPTIONS_GHC -fno-warn-deprecations #-}
5-- Module : Streamly.Internal.Data.Stream.Ahead
6-- Copyright : (c) 2017 Composewell Technologies
9-- Maintainer : streamly@composewell.com
10-- Stability : experimental
13-- To run examples in this module:
15-- >>> import qualified Streamly.Prelude as Stream
16-- >>> import Control.Concurrent (threadDelay)
19-- threadDelay (n * 1000000) -- sleep for n seconds
20-- putStrLn (show n ++ " sec") -- print "n sec"
24module Streamly.Internal.Data.Stream.Ahead {-# DEPRECATED "Please use \"Streamly.Internal.Data.Stream.Concurrent\" from streamly package instead." #-}
33import Control.Concurrent.MVar (putMVar, takeMVar)
34import Control.Exception (assert)
35import Control.Monad (void, when)
36import Control.Monad.Base (MonadBase(..), liftBaseDefault)
37import Control.Monad.Catch (MonadThrow, throwM)
38-- import Control.Monad.Error.Class (MonadError(..))
39import Control.Monad.IO.Class (MonadIO(..))
40import Control.Monad.Reader.Class (MonadReader(..))
41import Control.Monad.State.Class (MonadState(..))
42import Control.Monad.Trans.Class (MonadTrans(lift))
43import Data.Heap (Heap, Entry(..))
44import Data.IORef (IORef, readIORef, atomicModifyIORef, writeIORef)
45import Data.Maybe (fromJust)
46import GHC.Exts (inline)
48import qualified Data.Heap as H
50import Streamly.Internal.Control.Concurrent
51 (MonadRunInIO, MonadAsync, askRunInIO, restoreM)
52import Streamly.Internal.Data.Stream.StreamK.Type (Stream)
54import qualified Streamly.Internal.Data.Stream.StreamK.Type as K
55 (foldStreamShared, cons, mkStream, foldStream, fromEffect
56 , nil, concatMapWith, fromPure, bindWith)
57import qualified Streamly.Internal.Data.Stream.StreamD.Type as D
58 (mapM, fromStreamK, toStreamK)
59import qualified Streamly.Internal.Data.Stream.Serial as Stream (toStreamK)
61import Streamly.Internal.Data.Stream.SVar.Generate
62import Streamly.Internal.Data.SVar
63import Prelude hiding (map)
65#include "Instances.hs"
68-- >>> :set -fno-warn-deprecations
69-- >>> import qualified Streamly.Prelude as Stream
70-- >>> import Control.Concurrent (threadDelay)
73-- threadDelay (n * 1000000) -- sleep for n seconds
74-- putStrLn (show n ++ " sec") -- print "n sec"
78{-# INLINABLE withLocal #-}
79withLocal :: MonadReader r m => (r -> r) -> Stream m a -> Stream m a
81 K.mkStream $ \st yld sng stp ->
82 let single = local f . sng
83 yieldk a r = local f $ yld a (withLocal f r)
84 in K.foldStream st yieldk single (local f stp) m
86-------------------------------------------------------------------------------
88-------------------------------------------------------------------------------
90-- Lookahead streams can execute multiple tasks concurrently, ahead of time,
91-- but always serve them in the same order as they appear in the stream. To
92-- implement lookahead streams efficiently we assign a sequence number to each
93-- task when the task is picked up for execution. When the task finishes, the
94-- output is tagged with the same sequence number and we rearrange the outputs
95-- in sequence based on that number.
97-- To explain the mechanism imagine that the current task at the head of the
98-- stream has a "token" to yield to the outputQueue. The ownership of the token
99-- is determined by the current sequence number is maintained in outputHeap.
100-- Sequence number is assigned when a task is queued. When a thread dequeues a
101-- task it picks up the sequence number as well and when the output is ready it
102-- uses the sequence number to queue the output to the outputQueue.
104-- The thread with current sequence number sends the output directly to the
105-- outputQueue. Other threads push the output to the outputHeap. When the task
106-- being queued on the heap is a stream of many elements we evaluate only the
107-- first element and keep the rest of the unevaluated computation in the heap.
108-- When such a task gets the "token" for outputQueue it evaluates and directly
109-- yields all the elements to the outputQueue without checking for the
112-- Note that no two outputs in the heap can have the same sequence numbers and
113-- therefore we do not need a stable heap. We have also separated the buffer
114-- for the current task (outputQueue) and the pending tasks (outputHeap) so
115-- that the pending tasks cannot interfere with the current task. Note that for
116-- a single task just the outputQueue is enough and for the case of many
117-- threads just a heap is good enough. However we balance between these two
118-- cases, so that both are efficient.
120-- For bigger streams it may make sense to have separate buffers for each
121-- stream. However, for singleton streams this may become inefficient. However,
122-- if we do not have separate buffers, then the streams that come later in
123-- sequence may hog the buffer, hindering the streams that are ahead. For this
124-- reason we have a single element buffer limitation for the streams being
125-- executed in advance.
127-- This scheme works pretty efficiently with less than 40% extra overhead
128-- compared to the Async streams where we do not have any kind of sequencing of
129-- the outputs. It is especially devised so that we are most efficient when we
130-- have short tasks and need just a single thread. Also when a thread yields
131-- many items it can hold lockfree access to the outputQueue and do it
134-- XXX Maybe we can start the ahead threads at a lower cpu and IO priority so
135-- that they do not hog the resources and hinder the progress of the threads in
138-- Left associated ahead expressions are expensive. We start a new SVar for
139-- each left associative expression. The queue is used only for right
140-- associated expression, we queue the right expression and execute the left.
141-- Thererefore the queue never has more than on item in it.
143-- XXX Also note that limiting concurrency for cases like "take 10" would not
144-- work well with left associative expressions, because we have no visibility
145-- about how much the left side of the expression would yield.
147-- XXX It may be a good idea to increment sequence numbers for each yield,
148-- currently a stream on the left side of the expression may yield many
149-- elements with the same sequene number. We can then use the seq number to
150-- enforce yieldMax and yieldLImit as well.
154-- * A worker should always ensure that it pushes all the consecutive items in
155-- the heap to the outputQueue especially the items on behalf of the workers
156-- that have already left when we were holding the token. This avoids deadlock
157-- conditions when the later workers completion depends on the consumption of
158-- earlier results. For more details see comments in the consumer pull side
161{-# INLINE underMaxHeap #-}
164 -> Heap (Entry Int (AheadHeapEntry Stream m a))
166underMaxHeap sv hp = do
167 (_, len) <- readIORef (outputQueue sv)
170 let maxHeap = case maxBufferLimit sv of
171 Limited lim -> Limited $
172 max 0 (lim - fromIntegral len)
173 Unlimited -> Unlimited
177 active <- readIORef (workerCount sv)
178 return $ H.size hp + active <= fromIntegral lim
179 Unlimited -> return True
186 -> IORef (Heap (Entry Int (AheadHeapEntry Stream m a)) , Maybe Int)
188preStopCheck sv heap =
189 -- check the stop condition under a lock before actually
190 -- stopping so that the whole herd does not stop at once.
191 withIORef heap $ \(hp, _) -> do
192 heapOk <- underMaxHeap sv hp
193 takeMVar (workerStopMVar sv)
195 putMVar (workerStopMVar sv) ()
198 putMVar (workerStopMVar sv) ()
202 case yieldRateInfo sv of
205 rateOk <- isBeyondMaxRate sv yinfo
206 if rateOk then continue else stop
210 IORef ([Stream m a], Int)
215abortExecution q sv winfo m = do
216 reEnqueueAhead sv q m
217 incrementYieldLimit sv
220-- XXX In absence of a "noyield" primitive (i.e. do not pre-empt inside a
221-- critical section) from GHC RTS, we have a difficult problem. Assume we have
222-- a 100,000 threads producing output and queuing it to the heap for
223-- sequencing. The heap can be drained only by one thread at a time, any thread
224-- that finds that heap can be drained now, takes a lock and starts draining
225-- it, however the thread may get prempted in the middle of it holding the
226-- lock. Since that thread is holding the lock, the other threads cannot pick
227-- up the draining task, therefore they proceed to picking up the next task to
228-- execute. If the draining thread could yield voluntarily at a point where it
229-- has released the lock, then the next threads could pick up the draining
230-- instead of executing more tasks. When there are 100,000 threads the drainer
231-- gets a cpu share to run only 1:100000 of the time. This makes the heap
232-- accumulate a lot of output when we the buffer size is large.
234-- The solutions to this problem are:
235-- 1) make the other threads wait in a queue until the draining finishes
236-- 2) make the other threads queue and go away if draining is in progress
238-- In both cases we give the drainer a chance to run more often.
242 => IORef ([Stream m a], Int)
243 -> IORef (Heap (Entry Int (AheadHeapEntry Stream m a)), Maybe Int)
247 -> AheadHeapEntry Stream m a
249 -> Bool -- we are draining the heap before we stop
251processHeap q heap st sv winfo entry sno stopping = loopHeap sno entry
255 stopIfNeeded ent seqNo r = do
256 stopIt <- liftIO $ preStopCheck sv heap
259 -- put the entry back in the heap and stop
260 requeueOnHeapTop heap (Entry seqNo ent) seqNo
262 else runStreamWithYieldLimit True seqNo r
266 AheadEntryNull -> nextHeap seqNo
267 AheadEntryPure a -> do
268 -- Use 'send' directly so that we do not account this in worker
269 -- latency as this will not be the real latency.
270 -- Don't stop the worker in this case as we are just
271 -- transferring available results from heap to outputQueue.
272 void $ liftIO $ send sv (ChildYield a)
274 AheadEntryStream (RunInIO runin, r) ->
276 then stopIfNeeded ent seqNo r
278 res <- liftIO $ runin (runStreamWithYieldLimit True seqNo r)
282 nextHeap prevSeqNo = do
283 res <- liftIO $ dequeueFromHeapSeq heap (prevSeqNo + 1)
285 Ready (Entry seqNo hent) -> loopHeap seqNo hent
286 Clearing -> liftIO $ sendStop sv winfo
290 r <- liftIO $ preStopCheck sv heap
292 then liftIO $ sendStop sv winfo
293 else processWorkQueue prevSeqNo
294 else inline processWorkQueue prevSeqNo
296 processWorkQueue prevSeqNo = do
297 work <- dequeueAhead q
299 Nothing -> liftIO $ sendStop sv winfo
300 Just (m, seqNo) -> do
301 yieldLimitOk <- liftIO $ decrementYieldLimit sv
304 if seqNo == prevSeqNo + 1
305 then processWithToken q heap st sv winfo m seqNo
306 else processWithoutToken q heap st sv winfo m seqNo
307 else liftIO $ abortExecution q sv winfo m
309 -- We do not stop the worker on buffer full here as we want to proceed to
310 -- nextHeap anyway so that we can clear any subsequent entries. We stop
311 -- only in yield continuation where we may have a remaining stream to be
312 -- pushed on the heap.
313 singleStreamFromHeap seqNo a = do
314 void $ liftIO $ sendYield sv winfo (ChildYield a)
317 -- XXX when we have an unfinished stream on the heap we cannot account all
318 -- the yields of that stream until it finishes, so if we have picked up
319 -- and executed more actions beyond that in the parent stream and put them
320 -- on the heap then they would eat up some yield limit which is not
321 -- correct, we will think that our yield limit is over even though we have
322 -- to yield items from unfinished stream before them. For this reason, if
323 -- there are pending items in the heap we drain them unconditionally
324 -- without considering the yield limit.
325 runStreamWithYieldLimit continue seqNo r = do
326 _ <- liftIO $ decrementYieldLimit sv
327 if continue -- see comment above -- && yieldLimitOk
330 liftIO (incrementYieldLimit sv)
332 K.foldStreamShared st
333 (yieldStreamFromHeap seqNo)
334 (singleStreamFromHeap seqNo)
339 let ent = Entry seqNo (AheadEntryStream (runIn, r))
341 requeueOnHeapTop heap ent seqNo
342 incrementYieldLimit sv
345 yieldStreamFromHeap seqNo a r = do
346 continue <- liftIO $ sendYield sv winfo (ChildYield a)
347 runStreamWithYieldLimit continue seqNo r
349{-# NOINLINE drainHeap #-}
352 => IORef ([Stream m a], Int)
353 -> IORef (Heap (Entry Int (AheadHeapEntry Stream m a)), Maybe Int)
358drainHeap q heap st sv winfo = do
359 r <- liftIO $ dequeueFromHeap heap
361 Ready (Entry seqNo hent) ->
362 processHeap q heap st sv winfo hent seqNo True
363 _ -> liftIO $ sendStop sv winfo
365data HeapStatus = HContinue | HStop
366data WorkerStatus = Continue | Suspend
370 => IORef ([Stream m a], Int)
371 -> IORef (Heap (Entry Int (AheadHeapEntry Stream m a)), Maybe Int)
378processWithoutToken q heap st sv winfo m seqNo = do
379 -- we have already decremented the yield limit for m
381 liftIO (incrementYieldLimit sv)
382 -- If the stream stops without yielding anything, and we do not put
383 -- anything on heap, but if heap was waiting for this seq number
384 -- then it will keep waiting forever, because we are never going to
385 -- put it on heap. So we have to put a null entry on heap even when
387 toHeap AheadEntryNull
388 mrun = runInIO $ svarMrun sv
391 K.foldStreamShared st
394 toHeap $ AheadEntryStream (runIn, K.cons a r))
395 (toHeap . AheadEntryPure)
400 Continue -> workLoopAhead q heap st sv winfo
401 Suspend -> drainHeap q heap st sv winfo
405 -- XXX to reduce contention each CPU can have its own heap
407 -- Heap insertion is an expensive affair so we use a non CAS based
408 -- modification, otherwise contention and retries can make a thread
409 -- context switch and throw it behind other threads which come later in
411 newHp <- liftIO $ atomicModifyIORef heap $ \(hp, snum) ->
412 let hp' = H.insert (Entry seqNo ent) hp
413 in assert (heapIsSane snum seqNo) ((hp', snum), hp')
415 when (svarInspectMode sv) $
417 maxHp <- readIORef (maxHeapSize $ svarStats sv)
418 when (H.size newHp > maxHp) $
419 writeIORef (maxHeapSize $ svarStats sv) (H.size newHp)
421 heapOk <- liftIO $ underMaxHeap sv newHp
423 case yieldRateInfo sv of
424 Nothing -> return HContinue
428 rateOk <- liftIO $ workerRateControl sv yinfo info
430 then return HContinue
432 Nothing -> return HContinue
437 HContinue -> return Continue
438 HStop -> return Suspend
441data TokenWorkerStatus = TokenContinue Int | TokenSuspend
445 => IORef ([Stream m a], Int)
446 -> IORef (Heap (Entry Int (AheadHeapEntry Stream m a)), Maybe Int)
453processWithToken q heap st sv winfo action sno = do
454 -- Note, we enter this function with yield limit already decremented
455 -- XXX deduplicate stop in all invocations
457 liftIO (incrementYieldLimit sv)
458 return $ TokenContinue (sno + 1)
459 mrun = runInIO $ svarMrun sv
462 K.foldStreamShared st (yieldOutput sno) (singleOutput sno) stop action
466 TokenContinue seqNo -> loopWithToken seqNo
467 TokenSuspend -> drainHeap q heap st sv winfo
471 singleOutput seqNo a = do
472 continue <- liftIO $ sendYield sv winfo (ChildYield a)
474 then return $ TokenContinue (seqNo + 1)
476 liftIO $ updateHeapSeq heap (seqNo + 1)
479 -- XXX use a wrapper function around stop so that we never miss
480 -- incrementing the yield in a stop continuation. Essentiatlly all
481 -- "unstream" calls in this function must increment yield limit on stop.
482 yieldOutput seqNo a r = do
483 continue <- liftIO $ sendYield sv winfo (ChildYield a)
484 yieldLimitOk <- liftIO $ decrementYieldLimit sv
485 if continue && yieldLimitOk
488 liftIO (incrementYieldLimit sv)
489 return $ TokenContinue (seqNo + 1)
490 K.foldStreamShared st
497 let ent = Entry seqNo (AheadEntryStream (runIn, r))
498 liftIO $ requeueOnHeapTop heap ent seqNo
499 liftIO $ incrementYieldLimit sv
502 loopWithToken nextSeqNo = do
503 work <- dequeueAhead q
506 liftIO $ updateHeapSeq heap nextSeqNo
507 workLoopAhead q heap st sv winfo
509 Just (m, seqNo) -> do
510 yieldLimitOk <- liftIO $ decrementYieldLimit sv
511 let undo = liftIO $ do
512 updateHeapSeq heap nextSeqNo
513 reEnqueueAhead sv q m
514 incrementYieldLimit sv
517 if seqNo == nextSeqNo
520 liftIO (incrementYieldLimit sv)
521 return $ TokenContinue (seqNo + 1)
522 mrun = runInIO $ svarMrun sv
524 K.foldStreamShared st
531 TokenContinue seqNo1 -> loopWithToken seqNo1
532 TokenSuspend -> drainHeap q heap st sv winfo
535 -- To avoid a race when another thread puts something
536 -- on the heap and goes away, the consumer will not get
537 -- a doorBell and we will not clear the heap before
538 -- executing the next action. If the consumer depends
539 -- on the output that is stuck in the heap then this
540 -- will result in a deadlock. So we always clear the
541 -- heap before executing the next action.
542 undo >> workLoopAhead q heap st sv winfo
543 else undo >> drainHeap q heap st sv winfo
545-- XXX the yield limit changes increased the performance overhead by 30-40%.
546-- Just like AsyncT we can use an implementation without yeidlimit and even
547-- without pacing code to keep the performance higher in the unlimited and
550-- XXX The yieldLimit stuff is pretty invasive. We can instead do it by using
551-- three hooks, a pre-execute hook, a yield hook and a stop hook. In fact these
552-- hooks can be used for a more general implementation to even check predicates
553-- and not just yield limit.
555-- XXX we can remove the sv parameter as it can be derived from st
559 => IORef ([Stream m a], Int)
560 -> IORef (Heap (Entry Int (AheadHeapEntry Stream m a)), Maybe Int)
565workLoopAhead q heap st sv winfo = do
566 r <- liftIO $ dequeueFromHeap heap
568 Ready (Entry seqNo hent) ->
569 processHeap q heap st sv winfo hent seqNo False
570 Clearing -> liftIO $ sendStop sv winfo
572 -- Before we execute the next item from the work queue we check
573 -- if we are beyond the yield limit. It is better to check the
574 -- yield limit before we pick up the next item. Otherwise we
575 -- may have already started more tasks even though we may have
576 -- reached the yield limit. We can avoid this by taking active
577 -- workers into account, but that is not as reliable, because
578 -- workers may go away without picking up work and yielding a
581 -- Rate control can be done either based on actual yields in
582 -- the output queue or based on any yield either to the heap or
583 -- to the output queue. In both cases we may have one issue or
584 -- the other. We chose to do this based on actual yields to the
585 -- output queue because it makes the code common to both async
586 -- and ahead streams.
588 work <- dequeueAhead q
590 Nothing -> liftIO $ sendStop sv winfo
591 Just (m, seqNo) -> do
592 yieldLimitOk <- liftIO $ decrementYieldLimit sv
596 then processWithToken q heap st sv winfo m seqNo
597 else processWithoutToken q heap st sv winfo m seqNo
598 -- If some worker decremented the yield limit but then
599 -- did not yield anything and therefore incremented it
600 -- later, then if we did not requeue m here we may find
601 -- the work queue empty and therefore miss executing
602 -- the remaining action.
603 else liftIO $ abortExecution q sv winfo m
605-------------------------------------------------------------------------------
607-------------------------------------------------------------------------------
609-- XXX To be implemented. Use a linked queue like WAsync and put back the
610-- remaining computation at the back of the queue instead of the heap, and
611-- increment the sequence number.
613-- The only difference between forkSVarAsync and this is that we run the left
614-- computation without a shared SVar.
615forkSVarAhead :: MonadAsync m => Stream m a -> Stream m a -> Stream m a
616forkSVarAhead m1 m2 = K.mkStream $ \st yld sng stp -> do
617 sv <- newAheadVar st (concurrently m1 m2)
619 K.foldStream st yld sng stp $ Stream.toStreamK (fromSVar sv)
621 concurrently ma mb = K.mkStream $ \st yld sng stp -> do
622 runInIO <- askRunInIO
623 liftIO $ enqueue (fromJust $ streamVar st) (runInIO, mb)
624 K.foldStream st yld sng stp ma
627aheadK :: MonadAsync m => Stream m a -> Stream m a -> Stream m a
628aheadK m1 m2 = K.mkStream $ \st yld sng stp ->
630 Just sv | svarStyle sv == AheadVar -> do
631 runInIO <- askRunInIO
632 liftIO $ enqueue sv (runInIO, m2)
633 -- Always run the left side on a new SVar to avoid complexity in
634 -- sequencing results. This means the left side cannot further
635 -- split into more ahead computations on the same SVar.
636 K.foldStream st yld sng stp m1
637 _ -> K.foldStreamShared st yld sng stp (forkSVarAhead m1 m2)
639-- | XXX we can implement it more efficienty by directly implementing instead
640-- of combining streams using ahead.
642{-# SPECIALIZE consM :: IO a -> AheadT IO a -> AheadT IO a #-}
643consM :: MonadAsync m => m a -> AheadT m a -> AheadT m a
644consM m (AheadT r) = AheadT $ aheadK (K.fromEffect m) r
646------------------------------------------------------------------------------
648------------------------------------------------------------------------------
650-- | For 'AheadT' streams:
653-- (<>) = 'Streamly.Prelude.ahead'
654-- (>>=) = flip . 'Streamly.Prelude.concatMapWith' 'Streamly.Prelude.ahead'
657-- A single 'Monad' bind behaves like a @for@ loop with iterations executed
658-- concurrently, ahead of time, producing side effects of iterations out of
659-- order, but results in order:
662-- Stream.toList $ Stream.fromAhead $ do
663-- x <- Stream.fromList [2,1] -- foreach x in stream
664-- Stream.fromEffect $ delay x
670-- Nested monad binds behave like nested @for@ loops with nested iterations
671-- executed concurrently, ahead of time:
674-- Stream.toList $ Stream.fromAhead $ do
675-- x <- Stream.fromList [1,2] -- foreach x in stream
676-- y <- Stream.fromList [2,4] -- foreach y in stream
677-- Stream.fromEffect $ delay (x + y)
685-- The behavior can be explained as follows. All the iterations corresponding
686-- to the element @1@ in the first stream constitute one output stream and all
687-- the iterations corresponding to @2@ constitute another output stream and
688-- these two output streams are merged using 'ahead'.
690-- /Since: 0.3.0 ("Streamly")/
693newtype AheadT m a = AheadT {getAheadT :: Stream m a}
695instance MonadTrans AheadT where
697 lift = AheadT . K.fromEffect
699-- | A serial IO stream of elements of type @a@ with concurrent lookahead. See
700-- 'AheadT' documentation for more details.
702-- /Since: 0.3.0 ("Streamly")/
705type Ahead = AheadT IO
707------------------------------------------------------------------------------
709------------------------------------------------------------------------------
712{-# SPECIALIZE append :: AheadT IO a -> AheadT IO a -> AheadT IO a #-}
713append :: MonadAsync m => AheadT m a -> AheadT m a -> AheadT m a
714append (AheadT m1) (AheadT m2) = AheadT $ aheadK m1 m2
716instance MonadAsync m => Semigroup (AheadT m a) where
719------------------------------------------------------------------------------
721------------------------------------------------------------------------------
723instance MonadAsync m => Monoid (AheadT m a) where
724 mempty = AheadT K.nil
727------------------------------------------------------------------------------
729------------------------------------------------------------------------------
731{-# INLINE apAhead #-}
732apAhead :: MonadAsync m => AheadT m (a -> b) -> AheadT m a -> AheadT m b
733apAhead (AheadT m1) (AheadT m2) =
734 let f x1 = K.concatMapWith aheadK (K.fromPure . x1) m2
735 in AheadT $ K.concatMapWith aheadK f m1
737instance (Monad m, MonadAsync m) => Applicative (AheadT m) where
739 pure = AheadT . K.fromPure
744------------------------------------------------------------------------------
746------------------------------------------------------------------------------
748{-# INLINE bindAhead #-}
749{-# SPECIALIZE bindAhead ::
750 AheadT IO a -> (a -> AheadT IO b) -> AheadT IO b #-}
751bindAhead :: MonadAsync m => AheadT m a -> (a -> AheadT m b) -> AheadT m b
752bindAhead (AheadT m) f = AheadT $ K.bindWith aheadK m (getAheadT . f)
754instance MonadAsync m => Monad (AheadT m) where
760------------------------------------------------------------------------------
762------------------------------------------------------------------------------
764MONAD_COMMON_INSTANCES(AheadT, MONADPARALLEL)
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