hyperion-0.1.0.0: src/Hyperion/Main.hs
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Hyperion.Main
( defaultMain
, Mode(..)
, ConfigMonoid(..)
, ReportOutput(..)
, nullOutputPath
, defaultConfig
, defaultMainWith
) where
import Control.Applicative
import Control.Exception (Exception, throwIO, bracket)
import Control.Lens ((&), (.~), (%~), (%@~), (^..), folded, imapped, mapped, to)
import Control.Monad (unless, mzero, void)
import qualified Data.Aeson as JSON
import qualified Data.ByteString.Lazy.Char8 as BS
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.List (group, sort)
import Data.Maybe (fromMaybe)
import Data.Monoid
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (pack, Text, unpack)
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import Data.Time (getCurrentTime)
import Data.Typeable (Typeable)
import Data.Version (showVersion)
import GHC.Generics (Generic)
import Generics.Deriving.Monoid (memptydefault, mappenddefault)
import Hyperion.Analysis
import Hyperion.Benchmark
import Hyperion.Internal
import Hyperion.Measurement
import Hyperion.PrintReport
import Hyperion.Report
import Hyperion.Run
import qualified Options.Applicative as Options
import Paths_hyperion (version)
import System.Directory (createDirectoryIfMissing, doesDirectoryExist)
import System.Environment (getProgName)
import System.FilePath ((</>), (<.>))
import System.FilePath.Posix (hasTrailingPathSeparator)
import qualified System.IO as IO
data Mode = Version | List | Run | Analyze
deriving (Eq, Ord, Show)
-- | Specify a particular way of reporting the benchmark results.
data ReportOutput a = ReportPretty | ReportJson a | ReportJsonFlat a
deriving (Functor, Eq, Ord, Show)
-- | Context information about the benchmark.
data ContextInfo = ContextInfo
{ contextPackageName :: Text
, contextExecutableName :: Text
}
data ConfigMonoid = ConfigMonoid
{ configMonoidReportOutputs :: [ReportOutput FilePath]
, configMonoidMode :: First Mode
, configMonoidRaw :: First Bool
, configMonoidSamplingStrategy :: First SamplingStrategy
, configMonoidUserMetadata :: JSON.Object
, configMonoidSelectorPatterns :: [Text]
} deriving (Generic, Show)
instance Monoid ConfigMonoid where
mempty = memptydefault
mappend = mappenddefault
data Config = Config
{ configReportOutputs :: Set (ReportOutput FilePath)
, configMode :: Mode
, configRaw :: Bool
, configSamplingStrategy :: SamplingStrategy
, configUserMetadata :: JSON.Object
, configSelectorPatterns :: [Text]
} deriving (Generic, Show)
fromFirst :: a -> First a -> a
fromFirst x = fromMaybe x . getFirst
configFromMonoid :: ConfigMonoid -> Config
configFromMonoid ConfigMonoid{..} = Config
{ configReportOutputs =
if null configMonoidReportOutputs
then Set.singleton ReportPretty
else Set.fromList configMonoidReportOutputs
, configMode = fromFirst Analyze configMonoidMode
, configRaw = fromFirst False configMonoidRaw
, configSamplingStrategy = fromFirst defaultStrategy configMonoidSamplingStrategy
, configUserMetadata = configMonoidUserMetadata
, configSelectorPatterns = configMonoidSelectorPatterns
}
options :: Options.Parser ConfigMonoid
options = do
configMonoidReportOutputs <- many reportOutputParse
configMonoidMode <-
First <$> optional
(Options.flag' Version
(Options.long "version" <>
Options.hidden <>
Options.help "Display version information") <|>
Options.flag' List
(Options.long "list" <>
Options.short 'l' <>
Options.help "List benchmark names") <|>
Options.flag' Analyze
(Options.long "run" <>
Options.help "Run benchmarks and analyze them (default)") <|>
Options.flag' Run
(Options.long "no-analyze" <>
Options.help "Only run the benchmarks"))
configMonoidRaw <-
First <$> optional
(Options.switch
(Options.long "raw" <>
Options.help "Include raw measurement data in report."))
configMonoidUserMetadata <-
HashMap.fromList <$> many
(Options.option
parseKV
(Options.long "arg" <>
Options.metavar "KEY:VAL" <>
Options.help "Extra metadata to include in the report, in the format key:value."))
configMonoidSelectorPatterns <-
many
(pack <$> Options.argument Options.str
(Options.metavar "NAME..." ))
pure ConfigMonoid{..}
where
-- TODO allow setting this from CLI.
configMonoidSamplingStrategy = First Nothing
parseKV = do
txt <- Text.pack <$> Options.str
case Text.splitOn ":" txt of
[x,y] -> pure (x, JSON.String y)
_ -> mzero
reportOutputParse :: Options.Parser (ReportOutput FilePath)
reportOutputParse =
(ReportPretty <$ Options.flag' ()
(Options.long "pretty" <>
Options.help "Pretty prints the measurements on stdout.")) <|>
(ReportJson <$> Options.strOption
(Options.long "json" <>
Options.short 'j' <>
Options.help (unwords
["Where to write the json benchmarks output."
,"Can be a file name, a directory name or '-' for stdout."
]) <>
Options.metavar "PATH")) <|>
(ReportJsonFlat <$> Options.strOption
(Options.long "flat" <>
Options.short 'f' <>
Options.help (unwords
["Where to write the json benchmarks output."
,"Can be a file name, a directory name or '-' for stdout."
]) <>
Options.metavar "PATH"))
-- | The path to the null output file. This is @"nul"@ on Windows and
-- @"/dev/null"@ elsewhere.
nullOutputPath :: FilePath
#ifdef mingw32_HOST_OS
nullOutputPath = "nul"
#else
nullOutputPath = "/dev/null"
#endif
defaultConfig :: ConfigMonoid
defaultConfig = mempty
data DuplicateIdentifiers a = DuplicateIdentifiers [a]
instance (Show a, Typeable a) => Exception (DuplicateIdentifiers a)
instance Show a => Show (DuplicateIdentifiers a) where
show (DuplicateIdentifiers ids) = "Duplicate identifiers: " <> show ids
doList :: [Benchmark] -> IO ()
doList bks =
mapM_ Text.putStrLn $ bks^..folded.identifiers.to renderBenchmarkId
-- | Derive a 'SamplingStrategy' indexed by 'BenchmarkId' from the current
-- configuration.
indexedStrategy :: Config -> (BenchmarkId -> Maybe SamplingStrategy)
indexedStrategy Config{..} = case configSelectorPatterns of
[] -> uniform configSamplingStrategy
patts -> filtered f configSamplingStrategy
where
f bid = any (`Text.isPrefixOf` renderBenchmarkId bid) patts
doRun
:: (BenchmarkId -> Maybe SamplingStrategy)
-> [Benchmark]
-> IO (HashMap BenchmarkId Sample)
doRun strategy bks = do
let ids = bks^..folded.identifiers
-- Better asymptotics than nub.
unless (length (group (sort ids)) == length ids) $
throwIO $ DuplicateIdentifiers [ n | n:_:_ <- group (sort ids) ]
foldMap (runBenchmark strategy) bks
-- | Print the report.
printReport
:: ReportOutput IO.Handle
-> JSON.Object -- ^ Metadata
-> HashMap BenchmarkId Report
-> IO ()
-- XXX: should we print user metadata in pretty mode as well?
printReport ReportPretty _ report = printReports report
printReport (ReportJson h) metadata report =
BS.hPutStrLn h $ JSON.encode $
json metadata report
printReport (ReportJsonFlat h) metadata report =
BS.hPutStrLn h $ JSON.encode $
jsonFlat metadata report
-- | Open a 'Handle' for given report (if needed).
openReportHandle
:: ContextInfo
-> ReportOutput FilePath -> IO (ReportOutput IO.Handle)
openReportHandle _ ReportPretty = pure ReportPretty
openReportHandle cinfo (ReportJson path) =
ReportJson <$> openReportFileHandle cinfo path
openReportHandle cinfo (ReportJsonFlat path) =
ReportJsonFlat <$> openReportFileHandle cinfo path
openReportFileHandle :: ContextInfo -> FilePath -> IO IO.Handle
openReportFileHandle _ "-" = pure IO.stdout
openReportFileHandle cinfo path = do
let packageName = unpack $ contextPackageName cinfo
executableName = unpack $ contextExecutableName cinfo
dirExists <- doesDirectoryExist path
if dirExists ||
hasTrailingPathSeparator path
then do
let filename = packageName <.> executableName <.> "json"
createDirectoryIfMissing True path -- Creates the directory if needed.
IO.openFile (path </> filename) IO.WriteMode
else
IO.openFile path IO.WriteMode
closeReportHandle :: ReportOutput IO.Handle -> IO ()
closeReportHandle ReportPretty = return ()
closeReportHandle (ReportJson h) = IO.hClose h
closeReportHandle (ReportJsonFlat h) = IO.hClose h
doAnalyze
:: Config -- ^ Hyperion config.
-> ContextInfo -- ^ Benchmark context information.
-> [Benchmark] -- ^ Benchmarks to be run.
-> IO ()
doAnalyze Config{..} cinfo bks = do
results <- doRun (indexedStrategy Config{..}) bks
let strip
| configRaw = id
| otherwise = reportMeasurements .~ Nothing
report = results & imapped %@~ analyze & mapped %~ strip
now <- getCurrentTime
let -- TODO Use output of hostname(1) as reasonable default.
hostId = Nothing :: Maybe Text
metadata =
configUserMetadata
-- Prepend user metadata so that the user can rewrite @timestamp@,
-- for instance.
<> HashMap.fromList [ "timestamp" JSON..= now, "location" JSON..= hostId ]
void $ bracket
(mapM (openReportHandle cinfo)
$ Set.toList configReportOutputs)
(mapM_ closeReportHandle)
(mapM (\h -> printReport h metadata report))
defaultMainWith
:: ConfigMonoid -- ^ Preset Hyperion config.
-> String -- ^ Package name, user provided.
-> [Benchmark] -- ^ Benchmarks to be run.
-> IO ()
defaultMainWith presetConfig packageName bks = do
executableName <- getProgName -- Name of the executable that launched the benches.
cmdlineConfig <-
Options.execParser
(Options.info
(Options.helper <*> options)
Options.fullDesc)
let config = configFromMonoid (cmdlineConfig <> presetConfig)
cinfo = ContextInfo
{ contextPackageName = pack packageName
, contextExecutableName = pack executableName
}
case config of
Config{..} -> case configMode of
Version -> putStrLn $ "Hyperion " <> showVersion version
List -> doList bks
Run -> do
_ <- doRun (indexedStrategy config) bks
return ()
Analyze -> doAnalyze config cinfo bks
defaultMain
:: String -- ^ Package name, user provided.
-> [Benchmark] -- ^ Benchmarks to be run.
-> IO ()
defaultMain = defaultMainWith defaultConfig