{-# LANGUAGE PatternGuards, RecordWildCards #-} module Development.Shake.Report(ReportEntry(..), ReportTrace(..), buildReport) where import General.Base import Control.Arrow import Control.Monad import Data.Char import Data.Function import Data.List import System.FilePath import Paths_shake import qualified Data.ByteString.Lazy.Char8 as LBS data ReportEntry = ReportEntry {repName :: String, repBuilt :: Int, repChanged :: Int, repDepends :: [Int], repExecution :: Double, repTraces :: [ReportTrace]} data ReportTrace = ReportTrace {repCommand :: String, repStart :: Double, repStop :: Double} repTime ReportTrace{..} = repStop - repStart -- | Generates an report given some build system profiling data. buildReport :: FilePath -> [ReportEntry] -> IO () buildReport out xs | takeExtension out == ".js" = writeFile out $ "var shake = \n" ++ reportJSON xs | takeExtension out == ".json" = writeFile out $ reportJSON xs | takeExtension out == ".trace" = writeFile out $ reportTrace xs | out == "-" = putStr $ unlines $ reportSummary xs | otherwise = LBS.writeFile out =<< reportHTML xs reportSummary :: [ReportEntry] -> [String] reportSummary xs = ["* This database has tracked " ++ show (maximum (0 : map repChanged xs) + 1) ++ " runs." ,let f = show . length in "* There are " ++ f xs ++ " rules (" ++ f ls ++ " rebuilt in the last run)." ,let f = show . sum . map (length . repTraces) in "* Building required " ++ f xs ++ " traced commands (" ++ f ls ++ " in the last run)." ,"* The total (unparallelised) build time is " ++ showTime (sum $ map repExecution xs) ++ " of which " ++ showTime (sum $ map repTime $ concatMap repTraces xs) ++ " is traced commands." ,let f = (\(a,b) -> showTime a ++ " (" ++ b ++ ")") . maximumBy (compare `on` fst) in "* The longest rule takes " ++ f (map (repExecution &&& repName) xs) ++ ", and the longest traced command takes " ++ f (map (repTime &&& repCommand) $ concatMap repTraces xs) ++ "." ,let sumLast = sum $ map repTime $ concatMap repTraces ls maxStop = maximum $ 0 : map repStop (concatMap repTraces ls) in "* Last run gave an average parallelism of " ++ showDP 2 (if maxStop == 0 then 0 else sumLast / maxStop) ++ " times over " ++ showTime(maxStop) ++ "." ] where ls = filter ((==) 0 . repBuilt) xs reportHTML :: [ReportEntry] -> IO LBS.ByteString reportHTML xs = do htmlDir <- getDataFileName "html" report <- LBS.readFile $ htmlDir "report.html" let f name | name == "data.js" = return $ LBS.pack $ "var shake = \n" ++ reportJSON xs | otherwise = LBS.readFile $ htmlDir name runTemplate f report reportTrace :: [ReportEntry] -> String reportTrace xs = jsonListLines $ showEntries 0 [y{repCommand=repName x} | x <- xs, y <- repTraces x] ++ showEntries 1 (concatMap repTraces xs) where showEntries pid xs = map (showEntry pid) $ snd $ mapAccumL alloc [] $ sortBy (compare `on` repStart) xs alloc as r | (a1,an:a2) <- break (\a -> repStop a <= repStart r) as = (a1++r:a2, (length a1,r)) | otherwise = (as++[r], (length as,r)) showEntry pid (tid, ReportTrace{..}) = jsonObject [("args","{}"), ("ph",show "X"), ("cat",show "target") ,("name",show repCommand), ("tid",show tid), ("pid",show pid) ,("ts",show $ 1000000*repStart), ("dur",show $ 1000000*(repStop-repStart))] reportJSON :: [ReportEntry] -> String reportJSON = jsonListLines . map showEntry where showEntry ReportEntry{..} = jsonObject $ [("name", show repName) ,("built", show repBuilt) ,("changed", show repChanged) ,("depends", show repDepends) ,("execution", show repExecution)] ++ [("traces", jsonList $ map showTrace repTraces) | not $ null repTraces] showTrace ReportTrace{..} = jsonObject [("command",show repCommand), ("start",show repStart), ("stop",show repStop)] jsonListLines xs = "[" ++ intercalate "\n," xs ++ "\n]" jsonList xs = "[" ++ intercalate "," xs ++ "]" jsonObject xs = "{" ++ intercalate ", " [show a ++ ":" ++ b | (a,b) <- xs] ++ "}" --------------------------------------------------------------------- -- TEMPLATE ENGINE -- | Template Engine. Perform the following replacements on a line basis: -- -- * ==> -- -- * ==> runTemplate :: Monad m => (FilePath -> m LBS.ByteString) -> LBS.ByteString -> m LBS.ByteString runTemplate ask = liftM LBS.unlines . mapM f . LBS.lines where link = LBS.pack "\n" `LBS.append` res `LBS.append` LBS.pack "\n" | Just file <- lbs_stripPrefix link y = do res <- grab file; return $ LBS.pack "" | otherwise = return x where y = LBS.dropWhile isSpace x grab = ask . takeWhile (/= '\"') . LBS.unpack lbs_stripPrefix :: LBS.ByteString -> LBS.ByteString -> Maybe LBS.ByteString lbs_stripPrefix prefix text = if a == prefix then Just b else Nothing where (a,b) = LBS.splitAt (LBS.length prefix) text