{-# LANGUAGE RecordWildCards, CPP, ViewPatterns, ForeignFunctionInterface, TupleSections #-}
module Development.Shake.Internal.Progress(
progress,
progressSimple, progressDisplay, progressTitlebar, progressProgram,
ProgressEntry(..), progressReplay, writeProgressReport
) where
import Control.Applicative
import Data.Tuple.Extra
import Control.Exception.Extra
import Control.Monad.Extra
import System.Directory
import System.Process
import System.FilePath
import Data.Char
import Data.IORef
import Data.List
import Data.Maybe
import Development.Shake.Internal.Options
import Development.Shake.Internal.Core.Types
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy.Char8 as LBS
import Numeric.Extra
import General.Template
import General.EscCodes
import General.Extra
import Development.Shake.Internal.Paths
import Data.Monoid
import System.Time.Extra
import qualified General.Ids as Ids
import Prelude
#ifdef mingw32_HOST_OS
import Foreign.C.String
#ifdef x86_64_HOST_ARCH
#define CALLCONV ccall
#else
#define CALLCONV stdcall
#endif
foreign import CALLCONV "Windows.h SetConsoleTitleW" c_setConsoleTitleW :: CWString -> IO Bool
#endif
progress :: Database -> Step -> IO Progress
progress Database{..} step = do
xs <- Ids.elems status
return $! foldl' f mempty $ map snd xs
where
g = floatToDouble
f s (Ready Result{..}) = if step == built
then s{countBuilt = countBuilt s + 1, timeBuilt = timeBuilt s + g execution}
else s{countSkipped = countSkipped s + 1, timeSkipped = timeSkipped s + g execution}
f s (Loaded Result{..}) = s{countUnknown = countUnknown s + 1, timeUnknown = timeUnknown s + g execution}
f s (Running _ r) =
let (d,c) = timeTodo s
t | Just Result{..} <- r = let d2 = d + g execution in d2 `seq` (d2,c)
| otherwise = let c2 = c + 1 in c2 `seq` (d,c2)
in s{countTodo = countTodo s + 1, timeTodo = t}
f s _ = s
newtype Mealy i a = Mealy {runMealy :: i -> (a, Mealy i a)}
instance Functor (Mealy i) where
fmap f (Mealy m) = Mealy $ \i -> case m i of
(x, m) -> (f x, fmap f m)
instance Applicative (Mealy i) where
pure x = let r = Mealy (const (x, r)) in r
Mealy mf <*> Mealy mx = Mealy $ \i -> case mf i of
(f, mf) -> case mx i of
(x, mx) -> (f x, mf <*> mx)
echoMealy :: Mealy i i
echoMealy = Mealy (,echoMealy)
scanMealy :: (a -> b -> a) -> a -> Mealy i b -> Mealy i a
scanMealy f z (Mealy m) = Mealy $ \i -> case m i of
(x, m) -> let z2 = f z x in (z2, scanMealy f z2 m)
oldMealy :: a -> Mealy i a -> Mealy i (a,a)
oldMealy old = scanMealy (\(_,old) new -> (old,new)) (old,old)
latch :: Mealy i (Bool, a) -> Mealy i a
latch s = fromJust <$> scanMealy f Nothing s
where f old (b,v) = Just $ if b then fromMaybe v old else v
iff :: Mealy i Bool -> Mealy i a -> Mealy i a -> Mealy i a
iff c t f = (\c t f -> if c then t else f) <$> c <*> t <*> f
decay :: Double -> Mealy i Double -> Mealy i Double -> Mealy i Double
decay f a b = scanMealy step 0 $ (,) <$> oldMealy 0 a <*> oldMealy 0 b
where step r ((a,a'),(b,b')) = if isNaN r then a' / b' else ((r*b) + f*(a'-a)) / (b + f*(b'-b))
formatMessage :: Double -> Double -> String
formatMessage secs perc =
(if isNaN secs || secs < 0 then "??s" else showMinSec $ ceiling secs) ++ " (" ++
(if isNaN perc || perc < 0 || perc > 100 then "??" else show $ floor perc) ++ "%)"
showMinSec :: Int -> String
showMinSec secs = (if m == 0 then "" else show m ++ "m" ++ ['0' | s < 10]) ++ show s ++ "s"
where (m,s) = divMod secs 60
liftA2' :: Applicative m => m a -> m b -> (a -> b -> c) -> m c
liftA2' a b f = liftA2 f a b
message :: Mealy (Double, Progress) (Double, Progress) -> Mealy (Double, Progress) (Double, Double, String)
message input = liftA3 (,,) time perc debug
where
progress = snd <$> input
secs = fst <$> input
debug = (\donePerSec ruleTime (todoKnown,todoUnknown) ->
"Progress: " ++
"((known=" ++ showDP 2 todoKnown ++ "s) + " ++
"(unknown=" ++ show todoUnknown ++ " * time=" ++ showDP 2 ruleTime ++ "s)) " ++
"(rate=" ++ showDP 2 donePerSec ++ "))")
<$> donePerSec <*> ruleTime <*> (timeTodo <$> progress)
done = timeBuilt <$> progress
donePerSec = iff ((==) 0 <$> done) (pure 1) perSecStable
where perSecStable = latch $ liftA2 (,) (uncurry (==) <$> oldMealy 0 done) perSecRaw
perSecRaw = decay 1.2 done secs
ruleTime = liftA2 weightedAverage
(f (decay 10) timeBuilt countBuilt)
(f (liftA2 (/)) (fst . timeTodo) (\Progress{..} -> countTodo - snd timeTodo))
where
weightedAverage (w1,x1) (w2,x2)
| w1 == 0 && w2 == 0 = 0
| otherwise = ((w1 *. x1) + (w2 *. x2)) / intToDouble (w1+w2)
where i *. d = if i == 0 then 0 else intToDouble i * d
f divide time count = let xs = count <$> progress in liftA2 (,) xs $ divide (time <$> progress) (intToDouble <$> xs)
todo = f <$> progress <*> ruleTime
where f Progress{..} ruleTime = fst timeTodo + (fromIntegral (snd timeTodo) * ruleTime)
time = liftA2 (/) todo donePerSec
perc = iff ((==) 0 <$> done) (pure 0) $
liftA2' done todo $ \done todo -> 100 * done / (done + todo)
progressDisplay :: Double -> (String -> IO ()) -> IO Progress -> IO ()
progressDisplay sample disp prog = do
disp "Starting..."
time <- offsetTime
catchJust (\x -> if x == ThreadKilled then Just () else Nothing)
(loop time $ message echoMealy)
(const $ do t <- time; disp $ "Finished in " ++ showDuration t)
where
loop :: IO Double -> Mealy (Double, Progress) (Double, Double, String) -> IO ()
loop time mealy = do
sleep sample
p <- prog
t <- time
((secs,perc,_debug), mealy) <- return $ runMealy mealy (t, p)
let done = countSkipped p + countBuilt p
let todo = done + countUnknown p + countTodo p
disp $
"Running for " ++ showDurationSecs t ++ " [" ++ show done ++ "/" ++ show todo ++ "]" ++
", predicted " ++ formatMessage secs perc ++
maybe "" (", Failure! " ++) (isFailure p)
loop time mealy
data ProgressEntry = ProgressEntry
{idealSecs :: Double, idealPerc :: Double
,actualSecs :: Double, actualPerc :: Double
}
isInvalid :: ProgressEntry -> Bool
isInvalid ProgressEntry{..} = isNaN actualSecs || isNaN actualPerc
progressReplay :: [(Double, Progress)] -> [ProgressEntry]
progressReplay [] = []
progressReplay ps = snd $ mapAccumL f (message echoMealy) ps
where
end = fst $ last ps
f a (time,p) = (a2, ProgressEntry (end - time) (time * 100 / end) secs perc)
where ((secs,perc,_),a2) = runMealy a (time,p)
writeProgressReport :: FilePath -> [(FilePath, [(Double, Progress)])] -> IO ()
writeProgressReport out (map (second progressReplay) -> xs)
| (bad,_):_ <- filter (any isInvalid . snd) xs = errorIO $ "Progress generates NaN for " ++ bad
| takeExtension out == ".js" = writeFile out $ "var shake = \n" ++ generateJSON xs
| takeExtension out == ".json" = writeFile out $ generateJSON xs
| out == "-" = putStr $ unlines $ generateSummary xs
| otherwise = LBS.writeFile out =<< generateHTML xs
generateSummary :: [(FilePath, [ProgressEntry])] -> [String]
generateSummary xs = flip concatMap xs $ \(file,xs) ->
["# " ++ file, f xs "Seconds" idealSecs actualSecs, f xs "Percent" idealPerc actualPerc]
where
levels = [100,90,80,50]
f xs lbl ideal actual = lbl ++ ": " ++ intercalate ", "
[show l ++ "% within " ++ show (ceiling $ maximum $ 0 : take ((length xs * l) `div` 100) diff) | l <- levels]
where diff = sort [abs $ ideal x - actual x | x <- xs]
generateHTML :: [(FilePath, [ProgressEntry])] -> IO LBS.ByteString
generateHTML xs = do
report <- readDataFileHTML "progress.html"
let f name | name == "progress-data.js" = return $ LBS.pack $ "var progress =\n" ++ generateJSON xs
| name == "version.js" = return $ LBS.pack $ "var version = " ++ show shakeVersionString
| otherwise = readDataFileHTML name
runTemplate f report
generateJSON :: [(FilePath, [ProgressEntry])] -> String
generateJSON = concat . jsonList . map ((++"}") . unlines . f)
where
f (file,ps) =
("{\"name\":" ++ show (takeFileName file) ++ ", \"values\":") :
indent (jsonList $ map g ps)
shw = showDP 1
g ProgressEntry{..} = jsonObject
[("idealSecs",shw idealSecs),("idealPerc",shw idealPerc)
,("actualSecs",shw actualSecs),("actualPerc",shw actualPerc)]
indent = map (" "++)
jsonList xs = zipWith (:) ('[':repeat ',') xs ++ ["]"]
jsonObject xs = "{" ++ intercalate ", " [show a ++ ":" ++ b | (a,b) <- xs] ++ "}"
progressTitlebar :: String -> IO ()
progressTitlebar x = unlessM win lin
where
#ifdef mingw32_HOST_OS
win = withCWString x c_setConsoleTitleW
#else
win = return False
#endif
lin = whenM checkEscCodes $ BS.putStr $ BS.pack $ escWindowTitle x
progressProgram :: IO (String -> IO ())
progressProgram = do
exe <- findExecutable "shake-progress"
case exe of
Nothing -> return $ const $ return ()
Just exe -> do
lastArgs <- newIORef Nothing
return $ \msg -> do
let failure = " Failure! " `isInfixOf` msg
let perc = let (a,b) = break (== '%') msg
in if null b then "" else reverse $ takeWhile isDigit $ reverse a
let state | perc == "" = "NoProgress"
| failure = "Error"
| otherwise = "Normal"
let args = ["--title=" ++ msg, "--state=" ++ state] ++ ["--value=" ++ perc | perc /= ""]
same <- atomicModifyIORef lastArgs $ \old -> (Just args, old == Just args)
unless same $ void $ rawSystem exe args
progressSimple :: IO Progress -> IO ()
progressSimple p = do
program <- progressProgram
progressDisplay 5 (\s -> progressTitlebar s >> program s) p