packages feed

hcheckers-0.1.0.1: src/AI/AlphaBeta/Persistent.hs

{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE TemplateHaskell #-}

module AI.AlphaBeta.Persistent
  (lookupFile,
   putRecordFile,
   putStatsFile,
   putWriteQueue,
   checkWriteQueue,
   initFile,
   -- checkDataFile',
   loadIndexIO
  ) where

import Control.Monad
import Control.Monad.State
import Control.Monad.Catch (catch, SomeException)
import Control.Concurrent.STM
import qualified Data.HashPSQ as PQ
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.Map as M
import Data.Maybe
import Data.Word
import Data.Text.Format.Heavy
import Data.Store
import Data.Bits.Coded
import Data.Bits.Coding
import Data.Bytes.Put
import System.Clock
import Text.Printf
import GHC.Generics
import System.Posix.Types
import qualified System.IO.RandomAccessFile as File
import System.Log.Heavy
import System.Log.Heavy.TH

import Core.Types
import Core.Board
import qualified Core.Monitoring as Monitoring
import AI.AlphaBeta.Types

maxPieces :: Integer
maxPieces = 30

encodeBoard :: Board -> B.ByteString
encodeBoard board = runPutS $ runEncode $ encodeB board
  where

    encodePiece Nothing =
      putBit False
    encodePiece (Just (Piece Man First)) = do
      putBit True
      putBit False
      putBit False
    encodePiece (Just (Piece Man Second)) = do
      putBit True
      putBit False
      putBit True
    encodePiece (Just (Piece King First)) = do
      putBit True
      putBit True
      putBit False
    encodePiece (Just (Piece King Second)) = do
      putBit True
      putBit True
      putBit True

    encodeB b = do
      forM_ (allPieces b) encodePiece
      

sizeOf :: Data.Store.Store a => a -> ByteCount
sizeOf a =
  case Data.Store.size of
    Data.Store.VarSize fn -> fromIntegral $ fn a
    Data.Store.ConstSize n -> fromIntegral n

unexistingBlock :: IndexBlockNumber
unexistingBlock = maxBound

putCleanupQueue :: CleanupQueue -> QueueKey -> TimeSpec -> STM ()
putCleanupQueue var key now = modifyTVar var $ \queue ->
    let cleanupDelay = TimeSpec {sec=10, nsec=0}
    in  PQ.insert key (now + cleanupDelay) () queue

checkCleanupQueue :: CleanupQueue -> TimeSpec -> STM (Maybe QueueKey)
checkCleanupQueue var now = do
  queue <- readTVar var
  case PQ.minView queue of
    Nothing -> return Nothing
    Just (key, time, _, queue') ->
      if time < now
        then do
          writeTVar var queue'
          return $ Just key
        else return Nothing

putWriteQueue :: WriteQueue -> (Board, StorageValue) -> STM ()
putWriteQueue = writeTChan

checkWriteQueue :: WriteQueue -> STM (Maybe (Board, StorageValue))
checkWriteQueue = tryReadTChan

getFd :: FileType -> Storage FileDescriptor
getFd file = do
  fh <- getFh file
  return $ fhHandle fh

getFh :: FileType -> Storage FHandle
getFh file = do
  let selector = case file of
                   IndexFile -> ssIndex
                   DataFile -> ssData
  st <- get
  case selector st of
    Nothing -> fail "getFh: file is not open"
    Just fh -> return fh

updateFh :: FileType -> (FHandle -> FHandle) -> Storage ()
updateFh IndexFile fn = do
  st <- get
  case ssIndex st of
    Nothing -> fail "updateFh: index file is not open"
    Just fh -> put $ st {ssIndex = Just $ fn fh}
updateFh DataFile fn = do
  st <- get
  case ssData st of
    Nothing -> fail "updateFh: data file is not open"
    Just fh -> put $ st {ssData = Just $ fn fh}

tell :: FileType -> Storage FileOffset
tell file = do
  fh <- getFh file
  return $ fhOffset fh

seek :: FileType -> FileOffset -> Storage ()
seek file offset = do
  updateFh file $ \fh -> fh {fhOffset = offset}

readBytes :: FileType -> ByteCount -> Storage B.ByteString
readBytes file size = do
  fd <- getFd file
  currentOffset <- tell file
  result <- liftIO $ File.readBytes fd (fromIntegral currentOffset) (fromIntegral size)
  seek file $ fromIntegral size + currentOffset
  return result
  
writeBytes :: FileType -> B.ByteString -> Storage ()
writeBytes file bstr = do
  fd <- getFd file
  currentOffset <- tell file
  result <- liftIO $ File.writeBytes fd (fromIntegral currentOffset) bstr
  let size = B.length bstr
  seek file $ fromIntegral size + currentOffset

flush :: Storage ()
flush = return ()
--   fd <- getFd
--   handle <- liftIO $ fdToHandle fd
--   liftIO $ hFlush handle

isEof :: Storage Bool
isEof = return False
--   fd <- getFd
--   handle <- liftIO $ fdToHandle fd
--   liftIO $ hIsEOF handle

-- | Read an item of appropriate type from file handle.
-- Warning: this cannot work with data types which size is variable.
readData :: forall a. Data.Store.Store a => FileType -> Storage a
readData file = do
  bstr <- readBytes file (fromIntegral $ sizeOf (error "unknown data size to read!" :: a))
  when (B.null bstr) $ do
    offset <- tell file
    fail $ "readData: unexpected EOF, offset " ++ show offset
  liftIO $ Data.Store.decodeIO bstr

writeData :: forall a. Data.Store.Store a => FileType -> a -> Storage ()
writeData file a = do
  let bstr = Data.Store.encode a
  writeBytes file bstr

-- | Read an item of appropriate type from file handle.
-- Assumes there is data size in Word16 format before data itself.
readDataSized :: forall a. Data.Store.Store a => FileType -> Storage a
readDataSized file = do
  size <- readData file :: Storage Word16
  bstr <- readBytes file (fromIntegral size)
  when (B.null bstr) $ do
    offset <- tell file
    fail $ "readDataSized: zero data size, offset " ++ show offset
  liftIO $ Data.Store.decodeIO bstr

writeDataSized :: forall a. Data.Store.Store a => FileType -> a -> Storage ()
writeDataSized file a = do
  let bstr = Data.Store.encode a
  let size = (fromIntegral $ B.length bstr) :: Word16
  writeData file size
  writeBytes file bstr

dataHeaderSize :: FileOffset
dataHeaderSize = fromIntegral $ sizeOf (0 :: DataBlockNumber)

indexHeaderSize :: FileOffset
indexHeaderSize = fromIntegral $ sizeOf (0 :: IndexBlockNumber)

indexRecordSize :: FileOffset
indexRecordSize = fromIntegral (sizeOf (0 :: DataBlockNumber) + sizeOf (0 :: IndexBlockNumber))

indexBlockSize :: BoardSize -> FileOffset
indexBlockSize (nrows, ncols) = 256 * indexRecordSize

dataBlockSize :: FileOffset
dataBlockSize = 128

calcIndexBlockOffset :: BoardSize -> IndexBlockNumber -> FileOffset
calcIndexBlockOffset bsize n = indexHeaderSize + indexBlockSize bsize * fromIntegral n

calcDataBlockOffset :: DataBlockNumber -> FileOffset
calcDataBlockOffset n = dataHeaderSize + dataBlockSize * fromIntegral n

calcIndexOffset :: BoardSize -> IndexBlockNumber -> Word8 -> FileOffset
calcIndexOffset bsize block char =
  calcIndexBlockOffset bsize block + fromIntegral char * indexRecordSize

data IndexHeader = IndexHeader {
    ihBlocksCount :: IndexBlockNumber
  }
  deriving (Show, Generic)

instance Data.Store.Store IndexHeader where
  size = ConstSize $ fromIntegral $ sizeOf (0 :: IndexBlockNumber)

  poke h =
    poke $ ihBlocksCount h

  peek = do
    n <- peek
    return $ IndexHeader n

data DataHeader = DataHeader {
    dhBlocksCount :: DataBlockNumber
  }
  deriving (Show, Generic)

instance Data.Store.Store DataHeader where
  size = ConstSize $ fromIntegral $ sizeOf (0 :: DataBlockNumber)

  poke h =
    poke $ dhBlocksCount h

  peek = do
    n <- peek
    return $ DataHeader n

data IndexRecord = IndexRecord {
    irIndexBlock :: IndexBlockNumber
  , irDataBlock :: DataBlockNumber
  }
  deriving (Show)

instance Store IndexRecord where
  size = ConstSize $ fromIntegral $ sizeOf (0 :: IndexBlockNumber) + sizeOf (0 :: DataBlockNumber)

  poke r = do
    poke $ irIndexBlock r
    poke $ irDataBlock r

  peek = do
    idxBlock <- peek
    dataBlock <- peek
    return $ IndexRecord idxBlock dataBlock

lookupFileB :: B.ByteString -> Storage (Maybe PerBoardData)
lookupFileB bstr = do
    st <- get
    case ssData st of
      Nothing -> return Nothing
      Just _ -> loop 0 bstr
  where
    loop blockNumber bstr
      | B.length bstr == 1 = do
          bsize <- gets ssBoardSize
          let idxOffset = calcIndexOffset bsize blockNumber (B.head bstr)
          seek IndexFile idxOffset
          record <- readData IndexFile
          let dataBlockNumber = irDataBlock record
          if dataBlockNumber == unexistingBlock
            then return Nothing
            else do
                   let dataOffset = calcDataBlockOffset dataBlockNumber
                   seek DataFile dataOffset
                   value <- readDataSized DataFile
                   return $ Just value
      | otherwise = do
          bsize <- gets ssBoardSize
          let idxOffset = calcIndexOffset bsize blockNumber (B.head bstr)
          seek IndexFile idxOffset
          record <- readData IndexFile
          let nextBlockNumber = irIndexBlock record
          if nextBlockNumber == unexistingBlock
            then return Nothing
            else loop nextBlockNumber (B.tail bstr)
          

-- | Returns: (cached result, stats
lookupFile :: Board -> DepthParams -> Storage (Maybe PerBoardData)
lookupFile board depth = Monitoring.timed "cache.lookup.file" $ do
  mbRecord <- lookupFileB (encodeBoard board)
  case mbRecord of
    Nothing -> return Nothing
    Just record -> do
      return $
            if itemDepth record >= dpLast depth
              then Just record
              else Nothing

-- lookupStatsFile :: Board -> Storage (Maybe Stats)
-- lookupStatsFile board = Monitoring.timed "stats.lookup.file" $ do
--   mbItem <- lookupFileB (encodeBoard board)
--   return $ join $ boardStats `fmap` mbItem

putRecordFileB :: B.ByteString -> PerBoardData -> Storage ()
putRecordFileB bstr newData = do
    st <- get
    case ssData st of
      Nothing -> return ()
      Just _ -> tryBlock 0 bstr
  where
    tryBlock blockNumber bstr
      | B.length bstr == 1 = do
          bsize <- gets ssBoardSize
          let idxOffset = calcIndexOffset bsize blockNumber (B.head bstr)
          seek IndexFile idxOffset
          record <- readData IndexFile
          let dataBlockNumber = irDataBlock record
          if dataBlockNumber == unexistingBlock 
            then do
                 Monitoring.increment "storage.data.block.created"
                 newDataBlock <- createDataBlock
                 let record' = record {irDataBlock = newDataBlock}
                 seek IndexFile idxOffset
                 writeData IndexFile record'
                 seek DataFile $ calcDataBlockOffset newDataBlock
                 writeDataSized DataFile newData
                 return ()
            else do
                 Monitoring.increment "storage.data.block.reused"
                 let dataOffset = calcDataBlockOffset dataBlockNumber
                 seek DataFile dataOffset
                 oldData <- readDataSized DataFile
                              `catch`
                                (\(e :: SomeException) -> do
                                    $reportError "putRecordFileB: {}" (Single $ show e)
                                    return mempty
                                )
                 let newData' = oldData <> newData
                 seek DataFile dataOffset
                 writeDataSized DataFile newData'
                 return ()
      | otherwise = do
          bsize <- gets ssBoardSize
          let idxOffset = calcIndexOffset bsize blockNumber (B.head bstr)
          seek IndexFile idxOffset
          record <- readData IndexFile
          let nextBlockNumber = irIndexBlock record
          if nextBlockNumber == unexistingBlock
            then do
                 Monitoring.increment "storage.index.block.created"
                 newIndexBlock <- createIndexBlock
                 let record' = record {irIndexBlock = newIndexBlock}
                 seek IndexFile idxOffset
                 writeData IndexFile record'
                 tryBlock newIndexBlock (B.tail bstr)
            else do
                 Monitoring.increment "storage.index.block.reused"
                 tryBlock nextBlockNumber (B.tail bstr)
    
    createIndexBlock = do
      seek IndexFile 0
      header <- readData IndexFile
      let newBlockNumber = ihBlocksCount header + 1
      let header' = header {ihBlocksCount = newBlockNumber}
      bsize <- gets ssBoardSize
      seek IndexFile 0
      writeData IndexFile header'
      seek IndexFile $ calcIndexBlockOffset bsize newBlockNumber
      let empty = B.replicate (fromIntegral $ indexBlockSize bsize) 0xff
      writeBytes IndexFile empty
      return newBlockNumber

    createDataBlock = do
      seek DataFile 0
      header <- readData DataFile
      let newBlockNumber = dhBlocksCount header + 1
      let header' = header {dhBlocksCount = newBlockNumber}
      seek DataFile 0
      writeData DataFile header'
      seek DataFile $ calcDataBlockOffset newBlockNumber
      let empty = B.replicate (fromIntegral dataBlockSize) 0
      writeBytes DataFile empty
      return newBlockNumber

putRecordFile :: Board -> StorageValue -> Storage ()
putRecordFile board value = Monitoring.timed "cache.put.file" $ do
  let bstr = encodeBoard board
  putRecordFileB bstr value

putStatsFile :: Board -> Stats -> Storage ()
putStatsFile board stats = do
  return ()
--   let newData = PerBoardData mempty (Just stats)
--       bstr = encodeBoard board
--   putRecordFileB bstr newData

initFile :: Storage ()
initFile = do
  seek IndexFile 0
  writeData IndexFile $ IndexHeader 0
  bsize <- gets ssBoardSize
  let empty = B.replicate (fromIntegral $ indexBlockSize bsize) 0xff
  writeBytes IndexFile empty
  seek DataFile 0
  writeData DataFile $ DataHeader 0
  return ()

-- dumpFile :: FilePath -> IO ()
-- dumpFile path = withFile path ReadMode $ \file -> do
--       nBlocks <- readDataIO file :: IO Word16
--       printf "Number of blocks: %d\n" nBlocks
--       forM_ [0 .. hashIndiciesCount - 1] $ \hashIndexNr -> do
--         printf "Hash index #%d:\n" hashIndexNr
--         forM_ [0 .. hashesCount - 1] $ \hash -> do
--           lastBlockNr <- readDataIO file :: IO BlockNumber
--           when (lastBlockNr /= unexistingBlock) $ do
--             printf "  Hash %d, First:\tlast block #%d\n" hash lastBlockNr
--           lastBlockNr <- readDataIO file :: IO BlockNumber
--           when (lastBlockNr /= unexistingBlock) $ do
--             printf "  Hash %d, Second:\tlast block #%d\n" hash lastBlockNr
--       forM_ [0 .. nBlocks - 1] $ \blockNr -> do
--         let blockOffset = fromIntegral $ calcBlockOffset (fromIntegral blockNr)
--         hSeek file AbsoluteSeek blockOffset
--         printf "Block #%d, offset %d\n" blockNr blockOffset
--         header <- readDataIO file :: IO BlockHeader
--         let nRecords = bhRecordsCount header
--         printf "  Header: %s\n" (show header)
--         when (nRecords > 0) $ do
--           forM_ [0 .. nRecords - 1] $ \recordNr -> do
--             printf "    Record #%d:\n" recordNr
--             ((bk, depth),item) <- readDataSizedIO file :: IO (StorageKey, StorageValue)
--             printf "      Board key: %s\n" (show bk)
--             printf "      Depth: %d\n" depth
--             printf "      Value: %s\n" (show item)

readDataIO :: forall a h. (Data.Store.Store a, File.FileAccess h) => h -> File.Offset -> IO a
readDataIO file offset = do
  bstr <- File.readBytes file offset (fromIntegral $ sizeOf (error "unknown data size to read!" :: a))
  when (B.null bstr) $ do
    fail $ "readDataIO: unexpected EOF, offset " ++ show offset
  Data.Store.decodeIO bstr

readDataSizedIO :: forall a h. (Data.Store.Store a, File.FileAccess h) => h -> File.Offset -> IO a
readDataSizedIO file offset = do
  size <- readDataIO file offset :: IO Word16
  bstr <- File.readBytes file (offset + 2) (fromIntegral size)
  when (B.null bstr) $ do
    fail $ printf "readDataSizedIO: zero data size, offset %s, size %s" (show offset) (show size)
  Data.Store.decodeIO bstr

dumpIndexBlock :: File.FileAccess h => h -> BoardSize -> IndexBlockNumber -> IO ()
dumpIndexBlock h bsize n = do
  forM_ [0 .. 255] $ \char -> do
    let offset = fromIntegral $ calcIndexOffset bsize n char
    record <- readDataIO h offset
    when (irDataBlock record /= unexistingBlock || irIndexBlock record /= unexistingBlock) $
      printf "Char #%d: next index #%d, data block #%d\n" char (irIndexBlock record) (irDataBlock record)

checkDataFile :: FilePath -> IO ()
checkDataFile path = do
  let params = File.MMapedParams (1024*1024) False
  file <- File.initFile params path
  nBlocks <- readDataIO file 0 :: IO DataBlockNumber
  forM_ [0 .. nBlocks - 1] $ \i -> do
      let start = fromIntegral $ calcDataBlockOffset i
      size <- readDataIO file start :: IO Word16
      when (size > 0) $ do
        bstr <- File.readBytes file (start + 2) (fromIntegral size)
        record <- Data.Store.decodeIO bstr :: IO PerBoardData
        printf "Block #%d: data: %s\n" i (show record)

-- checkDataFile' :: FilePath -> IO ()
-- checkDataFile' path = do
--   let params = File.MMapedParams (1024*1024) False
--   file <- File.initFile params path
--   nBlocks <- readDataIO file 0 :: IO DataBlockNumber
--   forM_ [0 .. nBlocks - 1] $ \i -> do
--       let start = fromIntegral $ calcDataBlockOffset i
--       size <- readDataIO file start :: IO Word16
--       when (size > 0) $ do
--         bstr <- File.readBytes file (fromIntegral $ start + 2) (fromIntegral size)
--         record <- Data.Store.decodeIO bstr :: IO PerBoardData
--         case boardStats record of
--           Nothing -> return ()
--           Just stats -> 
--             when (statsCount stats > 10) $
--               printf "Block #%d: data: %s\n" i (show record)

data ParserState = ParserState {
    psIndex :: File.MMaped
  , psUnfinished :: M.Map IndexBlockNumber BL.ByteString
  , psFinished :: M.Map BL.ByteString DataBlockNumber
  }

loadIndex :: StateT ParserState IO ()
loadIndex = do
    index <- gets psIndex
    header <- liftIO $ readDataIO index 0
    let n = ihBlocksCount header
    forM_ [0 .. n-1] loadBlock
  where
    bsize = (8,8)

    loadBlock :: IndexBlockNumber -> StateT ParserState IO ()
    loadBlock i = do
      index <- gets psIndex
      finished <- gets psFinished
      liftIO $ printf "Block #%d; finished: %d\n" i (M.size finished)
      records <- forM [0 .. 255] $ \char -> do
        let offset = calcIndexOffset bsize i char
        liftIO $ readDataIO index (fromIntegral offset)
      unfinished <- gets psUnfinished
      modify $ \st -> st {psUnfinished = M.delete i (psUnfinished st)}
      let prefix = fromMaybe "" $ M.lookup i unfinished
      forM_ (zip [0..] records) $ \(j, record) -> do
        let prefix' = prefix `BL.append` BL.singleton j
        when (irDataBlock record /= unexistingBlock) $
          modify $ \st -> st {psFinished = M.insert prefix' (irDataBlock record) (psFinished st)}
        when (irIndexBlock record /= unexistingBlock) $
          modify $ \st -> st {psUnfinished = M.insert (irIndexBlock record) prefix' (psUnfinished st)}
  
loadIndexIO :: FilePath -> IO (M.Map BL.ByteString DataBlockNumber)
loadIndexIO indexPath = do 
  let params = File.MMapedParams (1024*1024) False
  index <- File.initFile params indexPath
  let st = ParserState index M.empty M.empty
  st' <- execStateT loadIndex st
  File.closeFile index
  putStrLn "index loaded."
  return $ psFinished st'

-- loadDataIO :: FilePath -> FilePath -> IO [(Board, PerBoardData)]
-- loadDataIO indexPath dataPath = do
--   index <- loadIndexIO indexPath
--   let params = File.MMapedParams (1024*1024) False
--   file <- File.initFile params
--   forM (M.assocs index) $ \(bstr, block) -> do