packages feed

conjure-0.1: src/Conjure/Piecemap.hs

{-# LANGUAGE CPP, TypeOperators, OverlappingInstances, RankNTypes #-}
{-
 Copyright (c) 2005-2006 Lemmih <lemmih@gmail.com>

 Permission to use, copy, modify, and distribute this software for any
 purpose with or without fee is hereby granted, provided that the above
 copyright notice and this permission notice appear in all copies.

 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Conjure.InterestTable
-- Copyright   :  (c) Lemmih, 2005
-- License     :  BSD-style
--
-- Maintainer  :  lemmih@gmail.com
-- Stability   :  experimental
-- Portability :  non-portable (requires DiffArrays)
--
--
-- The InterestTable keeps track of what pieces are
-- interesting. Interesting pieces are those pieces which few peers have
-- downloaded. If a piece is rare, it is a prime candidate for sharing
-- with others and most of the Bittorrent protocol relies on this fact
-- for fast piece retrieval.
--
-- This module provides a Table of interests. Every time we learn
-- something about the distribution of pieces, we update this table to
-- track the new information. Thus, we keep a table of things we should
-- be interested in. Of course, it is also possible to query a table to
-- find the next piece we should download.
-----------------------------------------------------------------------------
module Conjure.Piecemap
    ( drawPiecemap  -- :: Piecemap -> ShowS
    , emptyUsecount -- :: Int -> Usecount
    , emptyPiecemap -- :: Int -> Piecemap
    , mkPiecemap    -- :: Int -> ByteString -> Piecemap
    , fromPiecemap  -- :: Piecemap -> ByteString
    , findNewPieces -- :: Piecemap -- ^ Our pieces. @findNewPiece@ may not return (Just i) if this!i == True.
                    -- -> Piecemap -- ^ Remote pieces. @findNewPieces@ may not return (Just i) if this!i == False.
                    -- -> [Int]    -- ^ Lazy list of pieces we can get from the remote peer.
    , orderPieces
    , setPiecemapBit -- TVar Piecemap -> Int -> Bool -> STM ()
    , addPiecemap   -- :: Piecemap -> Usecount -> Usecount
    , addPiece      -- :: Int -> Usecount -> Usecount
    , delPiecemap   -- :: Piecemap -> Usecount -> Usecount
    , scanTorrent   -- :: Torrent -> TorrentHandle -> IO Piecemap
    ) where

import Data.List                ( groupBy, sortBy, group )
import Data.Ord                 ( comparing )
import Data.Word                ( Word8 )
import Data.Bits                ( Bits (..) )
import Data.Array.Base
import Data.Array.Diff
import Control.Monad.ST
import Control.Exception
import Control.Concurrent.STM
import System.Random            ( StdGen )

import Conjure.Types
import Conjure.Utils.Shuffle
import Conjure.Utils.SHA1 (sha1)
import Conjure.Torrent

import qualified Data.ByteString as BS
import Data.ByteString (ByteString)

#ifdef __CABAL_TEST__
import Test.QuickCheck hiding (evaluate)
#endif

import Control.Monad
import GHC.Exts
{-
-- import Data.Array.IO            ( IOUArray )
-- import System.IO.Unsafe         (unsafePerformIO)
-- Stolen from Data.Array.Diff:
-- If the array contains unboxed elements, then the elements of the
-- diff list may also recursively reference the array from inside
-- replaceDiffArray, so we must seq them too.
replaceDiffArray2 :: (MArray a e IO, Ix i)
                => IOToDiffArray a i e
                -> [(Int, e)]
                -> IO (IOToDiffArray a i e)
a `replaceDiffArray2` ies = do
    mapM_ (\(a,b) -> do evaluate a; evaluate b) ies
    a `replaceDiffArray` ies


instance IArray (IOToDiffArray IOUArray) Bool where
    bounds        a      = bounds (unsafeCoerce# a :: IOToDiffArray IOUArray i Int)
    unsafeArray   lu ies = unsafePerformIO $ newDiffArray lu ies
    unsafeAt      a i    = unsafePerformIO $ a `readDiffArray` i
    unsafeReplace a ies  = unsafePerformIO $ a `replaceDiffArray2` ies

instance (Ix ix, Show ix) => Show (DiffUArray ix Bool) where
  showsPrec = showsIArray
-}

drawPiecemap :: Piecemap -> ShowS
drawPiecemap piecemap
    = foldr (\e r -> draw e . r) id $ group $ elems piecemap
    where draw [True]        = showChar '#'
          draw lst@(True:_)  = shows (length lst) . showChar '#'
          draw [False]       = showChar '_'
          draw lst@(False:_) = shows (length lst) . showChar '_'


-- It's important that the sha1 check is strict.
-- Otherwise we can't garbage collect the pieces and
-- we'll end up with the entire file in memory.
-- FIXME: Skip all the pieces in a file when 'readPiece'' returns Nothing.
scanTorrent :: Torrent -> Backend -> IO Piecemap
scanTorrent torrent backend
    = fmap (listArray (0, nPieces-1)) (mapM checker [0 .. nPieces - 1])
    where nPieces = infoNumPieces torrent
          checker pNum
              = do pieceMb <- readPiece' backend pNum
                   case pieceMb of
                     Nothing -> return False
                     Just piece
                       -> return $! pieceCheckSum torrent pNum == sha1 piece

emptyUsecount :: Int -> Usecount
emptyUsecount l
    = listArray (0,l-1) (replicate l 0)

emptyPiecemap :: Int -> Piecemap
emptyPiecemap l
    = listArray (0,l-1) (replicate l False)

mkPiecemap :: Int -> ByteString -> Piecemap
mkPiecemap l bitmap
    = listArray (0,l-1) (take l (bsToBitmap (BS.unpack bitmap)))

bsToBitmap :: [Word8] -> [Bool]
bsToBitmap [] = []
bsToBitmap (x:xs)
    = foldl (\s n l -> s (testBit x n:l)) id [7, 6 .. 0] (bsToBitmap xs)

fromPiecemap :: Piecemap -> ByteString
fromPiecemap arr = BS.pack (bitmapToBS (elems arr))

bitmapToBS :: [Bool] -> [Word8]
bitmapToBS [] = []
bitmapToBS xs
    = foldl (\b a -> (b `shiftL` 1) .|. fromBool a) 0 (take 8 (byte ++ repeat False)):bitmapToBS bytes
    where (byte,bytes) = splitAt 8 xs

fromBool :: (Num t) => Bool -> t
fromBool False = 0
fromBool True = 1

findNewPieces :: Piecemap -- ^ Our pieces. @findNewPiece@ may not return (Just i) if this!i == True.
              -> Piecemap -- ^ Remote pieces. @findNewPieces@ may not return (Just i) if this!i == False.
              -> [Int]    -- ^ Lazy list of pieces we can get from the remote peer.
findNewPieces ourPieces remotePieces
    = assert (bounds ourPieces == bounds remotePieces) $
      loop 0
    where len = rangeSize $ bounds ourPieces
          loop n
              | n < len = let gotPiece = ourPieces `unsafeAt` n
                              isAvailable = remotePieces `unsafeAt` n
                          in if not gotPiece && isAvailable
                             then n:loop (n+1)
                             else loop (n+1)
              | otherwise = []

-- Arrange pieces in rarest first where equally rare pieces are shuffled.
orderPieces :: StdGen -> Usecount -> [Int] -> [Int]
orderPieces gen usecount wantedPieces
    = concatMap (shuffle gen)
      $ map (map snd) $ groupBy (\a b -> fst a == fst b)
      $ sortBy (comparing fst)
            [ (unsafeAt usecount idx,idx) | idx <- wantedPieces ]

runSTDiffU :: (Ix i) => (forall s. ST s (STUArray s i Int)) -> DiffUArray i Int
runSTDiffU st = runST (st >>= unsafeFreeze)

setPiecemapBit :: TVar Piecemap -> Int -> Bool -> STM ()
setPiecemapBit piecemapVar pieceNum status
    = do piecemap <- readTVar piecemapVar
         writeTVar piecemapVar (piecemap // [(pieceNum,status)])

addPiecemap :: Piecemap -> Usecount -> Usecount
addPiecemap = modPiecemap (+)

addPiece :: Int -> Usecount -> Usecount
addPiece idx usecount
    = let n = unsafeAt usecount idx
      in usecount // [(idx, n+1)]

delPiecemap :: Piecemap -> Usecount -> Usecount
delPiecemap = modPiecemap (-)

modPiecemap :: (Int -> Int -> Int) -> Piecemap -> Usecount -> Usecount
modPiecemap ac pieceArr countArr
    = assert (bounds pieceArr == bounds countArr) $
      runSTDiffU mkArray
    where len = rangeSize $ bounds countArr
          mkArray = do arr <- newArray_ (bounds countArr)
                       let loop n
                               | n < len = let newVal = unsafeAt countArr n `ac` if unsafeAt pieceArr n then 1 else 0
                                           in unsafeWrite arr n newVal >> loop (n+1)
                               | otherwise = return ()
                       loop 0
                       return arr


--------------------------------------------------------------
-- Tests.
--------------------------------------------------------------
#ifdef __CABAL_TEST__

instance Arbitrary Word8 where
    arbitrary = fmap fromIntegral (choose (0, 255) :: Gen Int)
instance Arbitrary (DiffArray Int Bool) where
    arbitrary = sized $ \n ->
                liftM (listArray (0,n-1)) (vector n)
    coarbitrary = undefined

instance Arbitrary (DiffUArray Int Int) where
    arbitrary = sized $ \n ->
                liftM (listArray (0,n-1)) (vector n)
    coarbitrary = undefined

{-
findNewPieces :: Piecemap -- ^ Our pieces. @findNewPiece@ may not return (Just i) if this!i == True.
              -> Piecemap -- ^ Remote pieces. @findNewPieces@ may not return (Just i) if this!i == False.
              -> [Int]    -- ^ Lazy list of pieces we can get from the remote peer.
-}

_prop_add_del piecemap usecount
    = assocs usecount == assocs (delPiecemap piecemap (addPiecemap piecemap usecount))

_prop_newPieces our remote
    = let newPieces = findNewPieces our remote
      in assocs our /= assocs remote ==> and [ not (our!p) && remote!p | p <- newPieces ]

_prop_identity piecemap
    = trivial (len==0) $ assocs piecemap == assocs (mkPiecemap len bs)
    where (l,u) = bounds piecemap
          len = u-l+1
          bs = fromPiecemap piecemap

_prop_bs_to_bitmap bs = trivial (null bs) $ bs == bitmapToBS (bsToBitmap bs)
_prop_bitmap_to_bs bitmap = trivial (null bitmap) $
                            bitmap == take (length bitmap) (bsToBitmap (bitmapToBS bitmap))


#endif