2#if __GLASGOW_HASKELL__ >= 800
3{-# OPTIONS_GHC -Wno-orphans #-}
8-- Copyright : (c) 2018 Harendra Kumar
11-- Maintainer : streamly@composewell.com
13import Control.DeepSeq (NFData(..), deepseq)
14import Data.Functor.Identity (Identity, runIdentity)
15import System.Random (randomRIO)
16import Data.Monoid (Last(..))
18import qualified GHC.Exts as GHC
19import qualified Streamly.Benchmark.Prelude as Ops
22import qualified Streamly.Data.Fold as FL
23import qualified Streamly.Memory.Array as A
24import qualified Streamly.Prelude as S
25import qualified Streamly.Internal.Data.Sink as Sink
27import qualified Streamly.Internal.Memory.Array as IA
28import qualified Streamly.Internal.Data.Fold as IFL
29import qualified Streamly.Internal.Prelude as IP
30import qualified Streamly.Internal.Data.Pipe as Pipe
34-------------------------------------------------------------------------------
36-------------------------------------------------------------------------------
38#if !MIN_VERSION_deepseq(1,4,3)
39instance NFData Ordering where rnf = (`seq` ())
42-- We need a monadic bind here to make sure that the function f does not get
43-- completely optimized out by the compiler in some cases.
45-- | Takes a fold method, and uses it with a default source.
46{-# INLINE benchIOSink #-}
48 :: (IsStream t, NFData b)
49 => String -> (t IO Int -> IO b) -> Benchmark
50benchIOSink name f = bench name $ nfIO $ randomRIO (1,1) >>= f . Ops.source
52{-# INLINE benchHoistSink #-}
54 :: (IsStream t, NFData b)
55 => String -> (t Identity Int -> IO b) -> Benchmark
56benchHoistSink name f =
57 bench name $ nfIO $ randomRIO (1,1) >>= f . Ops.sourceUnfoldr
59-- XXX once we convert all the functions to use this we can rename this to
61{-# INLINE benchIOSink1 #-}
62benchIOSink1 :: NFData b => String -> (Int -> IO b) -> Benchmark
63benchIOSink1 name f = bench name $ nfIO $ randomRIO (1,1) >>= f
65-- XXX We should be using sourceUnfoldrM for fair comparison with IO monad, but
66-- we can't use it as it requires MonadAsync constraint.
67{-# INLINE benchIdentitySink #-}
69 :: (IsStream t, NFData b)
70 => String -> (t Identity Int -> Identity b) -> Benchmark
71benchIdentitySink name f = bench name $ nf (f . Ops.sourceUnfoldr) 1
73-- | Takes a source, and uses it with a default drain/fold method.
74{-# INLINE benchIOSrc #-}
76 :: (t IO a -> SerialT IO a)
81 bench name $ nfIO $ randomRIO (1,1) >>= Ops.toNull t . f
83{-# INLINE benchIOSrc1 #-}
84benchIOSrc1 :: String -> (Int -> IO ()) -> Benchmark
85benchIOSrc1 name f = bench name $ nfIO $ randomRIO (1,1) >>= f
87{-# INLINE benchPure #-}
88benchPure :: NFData b => String -> (Int -> a) -> (a -> b) -> Benchmark
89benchPure name src f = bench name $ nfIO $ randomRIO (1,1) >>= return . f . src
91{-# INLINE benchPureSink #-}
92benchPureSink :: NFData b => String -> (SerialT Identity Int -> b) -> Benchmark
93benchPureSink name f = benchPure name Ops.sourceUnfoldr f
95-- XXX once we convert all the functions to use this we can rename this to
97{-# INLINE benchPureSink1 #-}
98benchPureSink1 :: NFData b => String -> (Int -> Identity b) -> Benchmark
99benchPureSink1 name f =
100 bench name $ nfIO $ randomRIO (1,1) >>= return . runIdentity . f
102{-# INLINE benchPureSinkIO #-}
105 => String -> (SerialT Identity Int -> IO b) -> Benchmark
106benchPureSinkIO name f =
107 bench name $ nfIO $ randomRIO (1, 1) >>= f . Ops.sourceUnfoldr
109{-# INLINE benchPureSrc #-}
110benchPureSrc :: String -> (Int -> SerialT Identity a) -> Benchmark
111benchPureSrc name src = benchPure name src (runIdentity . S.drain)
114mkString = "fromList [1" ++ concat (replicate Ops.value ",1") ++ "]"
116mkListString :: String
117mkListString = "[1" ++ concat (replicate Ops.value ",1") ++ "]"
120mkList = [1..Ops.value]
127 [ benchPureSink "id" id
128 , benchPureSink1 "eqBy" Ops.eqByPure
129 , benchPureSink "==" Ops.eqInstance
130 , benchPureSink "/=" Ops.eqInstanceNotEq
131 , benchPureSink1 "cmpBy" Ops.cmpByPure
132 , benchPureSink "<" Ops.ordInstance
133 , benchPureSink "min" Ops.ordInstanceMin
134 , benchPureSrc "IsList.fromList" Ops.sourceIsList
135 -- length is used to check for foldr/build fusion
136 , benchPureSink "length . IsList.toList" (length . GHC.toList)
137 , benchPureSrc "IsString.fromString" Ops.sourceIsString
138 , mkString `deepseq` (bench "readsPrec pure streams" $
139 nf Ops.readInstance mkString)
140 , mkString `deepseq` (bench "readsPrec Haskell lists" $
141 nf Ops.readInstanceList mkListString)
142 , benchPureSink "showsPrec pure streams" Ops.showInstance
143 , mkList `deepseq` (bench "showPrec Haskell lists" $
144 nf Ops.showInstanceList mkList)
145 , benchPureSink "foldl'" Ops.pureFoldl'
146 , benchPureSink "foldable/foldl'" Ops.foldableFoldl'
147 , benchPureSink "foldable/sum" Ops.foldableSum
148 , benchPureSinkIO "traversable/mapM" Ops.traversableMapM
150 , bgroup "generation"
151 [ -- Most basic, barely stream continuations running
152 benchIOSrc serially "unfoldr" Ops.sourceUnfoldr
153 , benchIOSrc serially "unfoldrM" Ops.sourceUnfoldrM
154 , benchIOSrc serially "intFromTo" Ops.sourceIntFromTo
155 , benchIOSrc serially "intFromThenTo" Ops.sourceIntFromThenTo
156 , benchIOSrc serially "integerFromStep" Ops.sourceIntegerFromStep
157 , benchIOSrc serially "fracFromThenTo" Ops.sourceFracFromThenTo
158 , benchIOSrc serially "fracFromTo" Ops.sourceFracFromTo
159 , benchIOSrc serially "fromList" Ops.sourceFromList
160 , benchIOSrc serially "fromListM" Ops.sourceFromListM
161 -- These are essentially cons and consM
162 , benchIOSrc serially "fromFoldable" Ops.sourceFromFoldable
163 , benchIOSrc serially "fromFoldableM" Ops.sourceFromFoldableM
165 , bgroup "elimination"
168 [ benchIOSink "foldrM" Ops.foldrMReduce
169 , benchIOSink "foldl'" Ops.foldl'Reduce
170 , benchIOSink "foldl1'" Ops.foldl1'Reduce
171 , benchIOSink "foldlM'" Ops.foldlM'Reduce
174 [ benchIdentitySink "foldrM" Ops.foldrMReduce
175 , benchIdentitySink "foldl'" Ops.foldl'Reduce
176 , benchIdentitySink "foldl1'" Ops.foldl1'Reduce
177 , benchIdentitySink "foldlM'" Ops.foldlM'Reduce
183 [ benchIOSink "foldrM" Ops.foldrMBuild
184 , benchIOSink "foldl'" Ops.foldl'Build
185 , benchIOSink "foldlM'" Ops.foldlM'Build
188 [ benchIdentitySink "foldrM" Ops.foldrMBuild
189 , benchIdentitySink "foldl'" Ops.foldl'Build
190 , benchIdentitySink "foldlM'" Ops.foldlM'Build
193 , benchIOSink "uncons" Ops.uncons
194 , benchIOSink "toNull" $ Ops.toNull serially
195 , benchIOSink "mapM_" Ops.mapM_
197 , benchIOSink "init" Ops.init
198 , benchIOSink "tail" Ops.tail
199 , benchIOSink "nullHeadTail" Ops.nullHeadTail
201 -- this is too low and causes all benchmarks reported in ns
202 -- , benchIOSink "head" Ops.head
203 , benchIOSink "last" Ops.last
204 -- , benchIOSink "lookup" Ops.lookup
205 , benchIOSink "find" Ops.find
206 , benchIOSink "findIndex" Ops.findIndex
207 , benchIOSink "elemIndex" Ops.elemIndex
209 -- this is too low and causes all benchmarks reported in ns
210 -- , benchIOSink "null" Ops.null
211 , benchIOSink "elem" Ops.elem
212 , benchIOSink "notElem" Ops.notElem
213 , benchIOSink "all" Ops.all
214 , benchIOSink "any" Ops.any
215 , benchIOSink "and" Ops.and
216 , benchIOSink "or" Ops.or
218 , benchIOSink "length" Ops.length
219 , benchHoistSink "length . generally" (Ops.length . IP.generally)
220 , benchIOSink "sum" Ops.sum
221 , benchIOSink "product" Ops.product
223 , benchIOSink "maximumBy" Ops.maximumBy
224 , benchIOSink "maximum" Ops.maximum
225 , benchIOSink "minimumBy" Ops.minimumBy
226 , benchIOSink "minimum" Ops.minimum
228 , benchIOSink "toList" Ops.toList
229 , benchIOSink "toListRev" Ops.toListRev
232 [ benchIOSink "drain" (S.fold FL.drain)
233 , benchIOSink "drainN" (S.fold (IFL.drainN Ops.value))
234 , benchIOSink "drainWhileTrue" (S.fold (IFL.drainWhile $ (<=) Ops.maxValue))
235 , benchIOSink "drainWhileFalse" (S.fold (IFL.drainWhile $ (>=) Ops.maxValue))
236 , benchIOSink "sink" (S.fold $ Sink.toFold Sink.drain)
237 , benchIOSink "last" (S.fold FL.last)
238 , benchIOSink "lastN.1" (S.fold (IA.lastN 1))
239 , benchIOSink "lastN.10" (S.fold (IA.lastN 10))
240 , benchIOSink "lastN.Max" (S.fold (IA.lastN Ops.maxValue))
241 , benchIOSink "length" (S.fold FL.length)
242 , benchIOSink "sum" (S.fold FL.sum)
243 , benchIOSink "product" (S.fold FL.product)
244 , benchIOSink "maximumBy" (S.fold (FL.maximumBy compare))
245 , benchIOSink "maximum" (S.fold FL.maximum)
246 , benchIOSink "minimumBy" (S.fold (FL.minimumBy compare))
247 , benchIOSink "minimum" (S.fold FL.minimum)
248 , benchIOSink "mean" (\s -> S.fold FL.mean (S.map (fromIntegral :: Int -> Double) s))
249 , benchIOSink "variance" (\s -> S.fold FL.variance (S.map (fromIntegral :: Int -> Double) s))
250 , benchIOSink "stdDev" (\s -> S.fold FL.stdDev (S.map (fromIntegral :: Int -> Double) s))
252 , benchIOSink "mconcat" (S.fold FL.mconcat . (S.map (Last . Just)))
253 , benchIOSink "foldMap" (S.fold (FL.foldMap (Last . Just)))
255 , benchIOSink "toList" (S.fold FL.toList)
256 , benchIOSink "toListRevF" (S.fold IFL.toListRevF)
257 , benchIOSink "toStream" (S.fold IP.toStream)
258 , benchIOSink "toStreamRev" (S.fold IP.toStreamRev)
259 , benchIOSink "writeN" (S.fold (A.writeN Ops.value))
261 , benchIOSink "index" (S.fold (FL.index Ops.maxValue))
262 , benchIOSink "head" (S.fold FL.head)
263 , benchIOSink "find" (S.fold (FL.find (== Ops.maxValue)))
264 , benchIOSink "findIndex" (S.fold (FL.findIndex (== Ops.maxValue)))
265 , benchIOSink "elemIndex" (S.fold (FL.elemIndex Ops.maxValue))
267 , benchIOSink "null" (S.fold FL.null)
268 , benchIOSink "elem" (S.fold (FL.elem Ops.maxValue))
269 , benchIOSink "notElem" (S.fold (FL.notElem Ops.maxValue))
270 , benchIOSink "all" (S.fold (FL.all (<= Ops.maxValue)))
271 , benchIOSink "any" (S.fold (FL.any (> Ops.maxValue)))
272 , benchIOSink "and" (\s -> S.fold FL.and (S.map (<= Ops.maxValue) s))
273 , benchIOSink "or" (\s -> S.fold FL.or (S.map (> Ops.maxValue) s))
275 , bgroup "fold-multi-stream"
276 [ benchIOSink1 "eqBy" Ops.eqBy
277 , benchIOSink1 "cmpBy" Ops.cmpBy
278 , benchIOSink "isPrefixOf" Ops.isPrefixOf
279 , benchIOSink "isSubsequenceOf" Ops.isSubsequenceOf
280 , benchIOSink "stripPrefix" Ops.stripPrefix
282 , bgroup "folds-transforms"
283 [ benchIOSink "drain" (S.fold FL.drain)
284 , benchIOSink "lmap" (S.fold (IFL.lmap (+1) FL.drain))
285 , benchIOSink "pipe-mapM"
286 (S.fold (IFL.transform (Pipe.mapM (\x -> return $ x + 1)) FL.drain))
288 , bgroup "folds-compositions" -- Applicative
290 benchIOSink "all,any" (S.fold ((,) <$> FL.all (<= Ops.maxValue)
291 <*> FL.any (> Ops.maxValue)))
292 , benchIOSink "sum,length" (S.fold ((,) <$> FL.sum <*> FL.length))
295 [ benchIOSink "mapM" (Ops.transformMapM serially 1)
296 , benchIOSink "compose" (Ops.transformComposeMapM serially 1)
297 , benchIOSink "tee" (Ops.transformTeeMapM serially 1)
298 , benchIOSink "zip" (Ops.transformZipMapM serially 1)
301 [ benchIOSink "mapM" (Ops.transformMapM serially 4)
302 , benchIOSink "compose" (Ops.transformComposeMapM serially 4)
303 , benchIOSink "tee" (Ops.transformTeeMapM serially 4)
304 , benchIOSink "zip" (Ops.transformZipMapM serially 4)
306 , bgroup "transformer"
307 [ benchIOSrc serially "evalState" Ops.evalStateT
308 , benchIOSrc serially "withState" Ops.withState
310 , bgroup "transformation"
311 [ benchIOSink "scanl" (Ops.scan 1)
312 , benchIOSink "scanl1'" (Ops.scanl1' 1)
313 , benchIOSink "map" (Ops.map 1)
314 , benchIOSink "fmap" (Ops.fmap 1)
315 , benchIOSink "mapM" (Ops.mapM serially 1)
316 , benchIOSink "mapMaybe" (Ops.mapMaybe 1)
317 , benchIOSink "mapMaybeM" (Ops.mapMaybeM 1)
318 , bench "sequence" $ nfIO $ randomRIO (1,1000) >>= \n ->
319 Ops.sequence serially (Ops.sourceUnfoldrMAction n)
320 , benchIOSink "findIndices" (Ops.findIndices 1)
321 , benchIOSink "elemIndices" (Ops.elemIndices 1)
322 , benchIOSink "reverse" (Ops.reverse 1)
323 , benchIOSink "reverse'" (Ops.reverse' 1)
324 , benchIOSink "foldrS" (Ops.foldrS 1)
325 , benchIOSink "foldrSMap" (Ops.foldrSMap 1)
326 , benchIOSink "foldrT" (Ops.foldrT 1)
327 , benchIOSink "foldrTMap" (Ops.foldrTMap 1)
328 , benchIOSink "tap" (Ops.tap 1)
329 , benchIOSink "tapRate 1 second" (Ops.tapRate 1)
330 , benchIOSink "pollCounts 1 second" (Ops.pollCounts 1)
331 , benchIOSink "tapAsync" (Ops.tapAsync 1)
332 , benchIOSink "tapAsyncS" (Ops.tapAsyncS 1)
334 , bgroup "transformationX4"
335 [ benchIOSink "scan" (Ops.scan 4)
336 , benchIOSink "scanl1'" (Ops.scanl1' 4)
337 , benchIOSink "map" (Ops.map 4)
338 , benchIOSink "fmap" (Ops.fmap 4)
339 , benchIOSink "mapM" (Ops.mapM serially 4)
340 , benchIOSink "mapMaybe" (Ops.mapMaybe 4)
341 , benchIOSink "mapMaybeM" (Ops.mapMaybeM 4)
342 -- , bench "sequence" $ nfIO $ randomRIO (1,1000) >>= \n ->
343 -- Ops.sequence serially (Ops.sourceUnfoldrMAction n)
344 , benchIOSink "findIndices" (Ops.findIndices 4)
345 , benchIOSink "elemIndices" (Ops.elemIndices 4)
348 [ benchIOSink "filter-even" (Ops.filterEven 1)
349 , benchIOSink "filter-all-out" (Ops.filterAllOut 1)
350 , benchIOSink "filter-all-in" (Ops.filterAllIn 1)
351 , benchIOSink "take-all" (Ops.takeAll 1)
352 , benchIOSink "takeWhile-true" (Ops.takeWhileTrue 1)
353 --, benchIOSink "takeWhileM-true" (Ops.takeWhileMTrue 1)
354 , benchIOSink "drop-one" (Ops.dropOne 1)
355 , benchIOSink "drop-all" (Ops.dropAll 1)
356 , benchIOSink "dropWhile-true" (Ops.dropWhileTrue 1)
357 --, benchIOSink "dropWhileM-true" (Ops.dropWhileMTrue 1)
358 , benchIOSink "dropWhile-false" (Ops.dropWhileFalse 1)
359 , benchIOSink "deleteBy" (Ops.deleteBy 1)
360 , benchIOSink "intersperse" (Ops.intersperse 1)
361 , benchIOSink "insertBy" (Ops.insertBy 1)
363 , bgroup "filteringX4"
364 [ benchIOSink "filter-even" (Ops.filterEven 4)
365 , benchIOSink "filter-all-out" (Ops.filterAllOut 4)
366 , benchIOSink "filter-all-in" (Ops.filterAllIn 4)
367 , benchIOSink "take-all" (Ops.takeAll 4)
368 , benchIOSink "takeWhile-true" (Ops.takeWhileTrue 4)
369 --, benchIOSink "takeWhileM-true" (Ops.takeWhileMTrue 4)
370 , benchIOSink "drop-one" (Ops.dropOne 4)
371 , benchIOSink "drop-all" (Ops.dropAll 4)
372 , benchIOSink "dropWhile-true" (Ops.dropWhileTrue 4)
373 --, benchIOSink "dropWhileM-true" (Ops.dropWhileMTrue 4)
374 , benchIOSink "dropWhile-false" (Ops.dropWhileFalse 4)
375 , benchIOSink "deleteBy" (Ops.deleteBy 4)
376 , benchIOSink "intersperse" (Ops.intersperse 4)
377 , benchIOSink "insertBy" (Ops.insertBy 4)
380 [ benchIOSrc1 "zip (2,x/2)" (Ops.zip (Ops.value `div` 2))
381 , benchIOSrc1 "zipM (2,x/2)" (Ops.zipM (Ops.value `div` 2))
382 , benchIOSrc1 "mergeBy (2,x/2)" (Ops.mergeBy (Ops.value `div` 2))
383 , benchIOSrc1 "serial (2,x/2)" (Ops.serial2 (Ops.value `div` 2))
384 , benchIOSrc1 "append (2,x/2)" (Ops.append2 (Ops.value `div` 2))
385 , benchIOSrc1 "serial (2,2,x/4)" (Ops.serial4 (Ops.value `div` 4))
386 , benchIOSrc1 "append (2,2,x/4)" (Ops.append4 (Ops.value `div` 4))
387 , benchIOSrc1 "wSerial (2,x/2)" Ops.wSerial2
388 , benchIOSrc1 "interleave (2,x/2)" Ops.interleave2
389 , benchIOSrc1 "roundRobin (2,x/2)" Ops.roundRobin2
391 , bgroup "concat-foldable"
392 [ benchIOSrc serially "foldMapWith" Ops.sourceFoldMapWith
393 , benchIOSrc serially "foldMapWithM" Ops.sourceFoldMapWithM
394 , benchIOSrc serially "foldMapM" Ops.sourceFoldMapM
395 , benchIOSrc serially "foldWithConcatMapId" Ops.sourceConcatMapId
397 , bgroup "concat-serial"
398 [ benchIOSrc1 "concatMapPure (2,x/2)" (Ops.concatMapPure 2 (Ops.value `div` 2))
399 , benchIOSrc1 "concatMap (2,x/2)" (Ops.concatMap 2 (Ops.value `div` 2))
400 , benchIOSrc1 "concatMap (x/2,2)" (Ops.concatMap (Ops.value `div` 2) 2)
401 , benchIOSrc1 "concatMapRepl (x/4,4)" Ops.concatMapRepl4xN
402 , benchIOSrc1 "concatUnfoldRepl (x/4,4)" Ops.concatUnfoldRepl4xN
404 , benchIOSrc1 "concatMapWithSerial (2,x/2)"
405 (Ops.concatMapWithSerial 2 (Ops.value `div` 2))
406 , benchIOSrc1 "concatMapWithSerial (x/2,2)"
407 (Ops.concatMapWithSerial (Ops.value `div` 2) 2)
409 , benchIOSrc1 "concatMapWithAppend (2,x/2)"
410 (Ops.concatMapWithAppend 2 (Ops.value `div` 2))
412 , bgroup "concat-interleave"
413 [ benchIOSrc1 "concatMapWithWSerial (2,x/2)"
414 (Ops.concatMapWithWSerial 2 (Ops.value `div` 2))
415 , benchIOSrc1 "concatMapWithWSerial (x/2,2)"
416 (Ops.concatMapWithWSerial (Ops.value `div` 2) 2)
417 , benchIOSrc1 "concatUnfoldInterleaveRepl (x/4,4)"
418 Ops.concatUnfoldInterleaveRepl4xN
419 , benchIOSrc1 "concatUnfoldRoundrobinRepl (x/4,4)"
420 Ops.concatUnfoldRoundrobinRepl4xN
422 -- scanl-map and foldl-map are equivalent to the scan and fold in the foldl
423 -- library. If scan/fold followed by a map is efficient enough we may not
424 -- need monolithic implementations of these.
426 [ benchIOSink "scanl-map" (Ops.scanMap 1)
427 , benchIOSink "foldl-map" Ops.foldl'ReduceMap
428 , benchIOSink "sum-product-fold" Ops.sumProductFold
429 , benchIOSink "sum-product-scan" Ops.sumProductScan
432 [ benchIOSink "scan-map" (Ops.scanMap 4)
433 , benchIOSink "drop-map" (Ops.dropMap 4)
434 , benchIOSink "drop-scan" (Ops.dropScan 4)
435 , benchIOSink "take-drop" (Ops.takeDrop 4)
436 , benchIOSink "take-scan" (Ops.takeScan 4)
437 , benchIOSink "take-map" (Ops.takeMap 4)
438 , benchIOSink "filter-drop" (Ops.filterDrop 4)
439 , benchIOSink "filter-take" (Ops.filterTake 4)
440 , benchIOSink "filter-scan" (Ops.filterScan 4)
441 , benchIOSink "filter-scanl1" (Ops.filterScanl1 4)
442 , benchIOSink "filter-map" (Ops.filterMap 4)
445 [ benchIOSrc serially "mapM" Ops.iterateMapM
446 , benchIOSrc serially "scan(1/100)" Ops.iterateScan
447 , benchIOSrc serially "scanl1(1/100)" Ops.iterateScanl1
448 , benchIOSrc serially "filterEven" Ops.iterateFilterEven
449 , benchIOSrc serially "takeAll" Ops.iterateTakeAll
450 , benchIOSrc serially "dropOne" Ops.iterateDropOne
451 , benchIOSrc serially "dropWhileFalse" Ops.iterateDropWhileFalse
452 , benchIOSrc serially "dropWhileTrue" Ops.iterateDropWhileTrue
456 [ bgroup "transformation"
457 [ benchIOSink "fmap" $ Ops.fmap' wSerially 1
460 , bgroup "zipSerially"
461 [ bgroup "transformation"
462 [ benchIOSink "fmap" $ Ops.fmap' zipSerially 1
465 , bgroup "zipAsyncly"
466 [ bgroup "transformation"
467 [ benchIOSink "fmap" $ Ops.fmap' zipAsyncly 1
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