-
Notifications
You must be signed in to change notification settings - Fork 166
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
1 parent
9503c22
commit c777f30
Showing
4 changed files
with
338 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,327 @@ | ||
-- | @futhark benchcmp@ | ||
module Futhark.CLI.Benchcmp (main) where | ||
|
||
import Control.Exception (catch) | ||
import Data.Bifunctor (Bifunctor (bimap, first, second)) | ||
import Data.ByteString.Lazy.Char8 qualified as LBS | ||
import Data.Either qualified as E | ||
import Data.List qualified as L | ||
import Data.Map qualified as M | ||
import Data.Text qualified as T | ||
import Data.Vector qualified as V | ||
import Futhark.Bench | ||
import Futhark.Util (showText) | ||
import Futhark.Util.Options (mainWithOptions) | ||
import Statistics.Sample qualified as S | ||
import System.Console.ANSI (hSupportsANSI) | ||
import System.IO (stdout) | ||
import Text.Printf (printf) | ||
|
||
-- | Record that summerizes a comparison between two benchmarks. | ||
data SpeedUp = SpeedUp | ||
{ -- | What factor the benchmark is improved by. | ||
speedup :: Double, | ||
-- | Memory usage. | ||
memoryUsage :: M.Map T.Text Double, | ||
-- | If the speedup was significant. | ||
significant :: Bool | ||
} | ||
deriving (Show) | ||
|
||
-- | Terminal colors used when printing the comparisons. Some of these are not | ||
-- colors ways of emphasising text. | ||
data Colors = Colors | ||
{ -- | The header color. | ||
header :: T.Text, | ||
-- | Okay color | ||
okblue :: T.Text, | ||
-- | A second okay color | ||
okgreen :: T.Text, | ||
-- | Warning color. | ||
warning :: T.Text, | ||
-- | When something fails. | ||
failing :: T.Text, | ||
-- | Default color. | ||
endc :: T.Text, | ||
-- | Bold text. | ||
bold :: T.Text, | ||
-- | Underline text. | ||
underline :: T.Text | ||
} | ||
|
||
-- | Colors to use for a terminal device. | ||
ttyColors :: Colors | ||
ttyColors = | ||
Colors | ||
{ header = "\ESC[95m", | ||
okblue = "\ESC[94m", | ||
okgreen = "\ESC[92m", | ||
warning = "\ESC[93m", | ||
failing = "\ESC[91m", | ||
endc = "\ESC[0m", | ||
bold = "\ESC[1m", | ||
underline = "\ESC[4m" | ||
} | ||
|
||
-- | Colors to use for a non-terminal device. | ||
nonTtyColors :: Colors | ||
nonTtyColors = | ||
Colors | ||
{ header = "", | ||
okblue = "", | ||
okgreen = "", | ||
warning = "", | ||
failing = "", | ||
endc = "", | ||
bold = "", | ||
underline = "" | ||
} | ||
|
||
-- | Reads a file without throwing an error. | ||
readFileSafely :: T.Text -> IO (Either T.Text LBS.ByteString) | ||
readFileSafely filepath = | ||
(Right <$> LBS.readFile (T.unpack filepath)) `catch` couldNotRead | ||
where | ||
couldNotRead e = pure $ Left $ showText (e :: IOError) | ||
|
||
-- | Converts DataResults to a Map with the text as a key. | ||
toDataResultsMap :: [DataResult] -> M.Map T.Text (Either T.Text Result) | ||
toDataResultsMap = M.fromList . fmap toTuple | ||
where | ||
toTuple (DataResult dataset dataResults) = (dataset, dataResults) | ||
|
||
-- | Converts BenchResults to a Map with the file path as a key. | ||
toBenchResultsMap :: | ||
[BenchResult] -> | ||
M.Map T.Text (M.Map T.Text (Either T.Text Result)) | ||
toBenchResultsMap = M.fromList . fmap toTuple | ||
where | ||
toTuple (BenchResult path dataResults) = | ||
(T.pack path, toDataResultsMap dataResults) | ||
|
||
-- | Given a file path to a json file which has the form of a futhark benchmark | ||
-- result, it will try to parse the file to a Map of Maps. The key | ||
-- in the outer most dictionary is a file path the inner key is the dataset. | ||
decodeFileBenchResultsMap :: | ||
T.Text -> | ||
IO (Either T.Text (M.Map T.Text (M.Map T.Text (Either T.Text Result)))) | ||
decodeFileBenchResultsMap path = do | ||
file <- readFileSafely path | ||
pure $ toBenchResultsMap <$> (file >>= (first T.pack . decodeBenchResults)) | ||
|
||
-- | Will return a text with an error saying there is a missing program in a | ||
-- given result. | ||
formatMissingProg :: T.Text -> T.Text -> T.Text -> T.Text | ||
formatMissingProg = ((T.pack .) .) . printf "In %s but not %s: program %s" | ||
|
||
-- | Will return a text with an error saying there is a missing dataset in a | ||
-- given result. | ||
formatMissingData :: T.Text -> T.Text -> T.Text -> T.Text -> T.Text | ||
formatMissingData = | ||
(((T.pack .) .) .) . printf "In %s but not %s: program %s dataset %s" | ||
|
||
-- | Will return texts that say there are a missing program. | ||
formatManyMissingProg :: T.Text -> T.Text -> [T.Text] -> [T.Text] | ||
formatManyMissingProg a_path b_path = | ||
zipWith3 formatMissingProg a_paths b_paths | ||
where | ||
a_paths = repeat a_path | ||
b_paths = repeat b_path | ||
|
||
-- | Will return texts that say there are missing datasets for a program. | ||
formatManyMissingData :: T.Text -> T.Text -> T.Text -> [T.Text] -> [T.Text] | ||
formatManyMissingData prog a_path b_path = | ||
L.zipWith4 formatMissingData a_paths b_paths progs | ||
where | ||
a_paths = repeat a_path | ||
b_paths = repeat b_path | ||
progs = repeat prog | ||
|
||
-- | Finds the keys two Maps does not have in common and returns a appropiate | ||
-- error based on the functioned passed. | ||
missingResults :: | ||
(T.Text -> T.Text -> [T.Text] -> [T.Text]) -> | ||
T.Text -> | ||
T.Text -> | ||
M.Map T.Text a -> | ||
M.Map T.Text b -> | ||
[T.Text] | ||
missingResults toMissingMap a_path b_path a_results b_results = missing | ||
where | ||
a_keys = M.keys a_results | ||
b_keys = M.keys b_results | ||
a_missing = toMissingMap a_path b_path $ a_keys L.\\ b_keys | ||
b_missing = toMissingMap b_path a_path $ b_keys L.\\ a_keys | ||
missing = a_missing `L.union` b_missing | ||
|
||
-- | Compares the memory usage of two results. | ||
computeMemoryUsage :: | ||
M.Map T.Text Int -> | ||
M.Map T.Text Int -> | ||
M.Map T.Text Double | ||
computeMemoryUsage a b = M.intersectionWith divide b $ M.filter (/= 0) a | ||
where | ||
divide x y = fromIntegral x / fromIntegral y | ||
|
||
-- | Compares two results and thereby computes the Speed Up records. | ||
compareResult :: Result -> Result -> SpeedUp | ||
compareResult a b = | ||
SpeedUp | ||
{ speedup = speedup', | ||
significant = significant', | ||
memoryUsage = memory_usage | ||
} | ||
where | ||
runResultToDouble :: RunResult -> Double | ||
runResultToDouble = fromIntegral . runMicroseconds | ||
toVector = V.fromList . (runResultToDouble <$>) . runResults | ||
a_memory_usage = memoryMap a | ||
b_memory_usage = memoryMap b | ||
a_run_results = toVector a | ||
b_run_results = toVector b | ||
a_std = S.stdDev a_run_results | ||
b_std = S.stdDev b_run_results | ||
a_mean = S.mean a_run_results | ||
b_mean = S.mean b_run_results | ||
diff = abs $ a_mean - b_mean | ||
speedup' = a_mean / b_mean | ||
significant' = diff > a_std / 2 + b_std / 2 | ||
memory_usage = computeMemoryUsage a_memory_usage b_memory_usage | ||
|
||
-- | Given two Maps containing datasets as keys and results as values, compare | ||
-- the results and return the errors in a tuple. | ||
compareDataResults :: | ||
T.Text -> | ||
T.Text -> | ||
T.Text -> | ||
M.Map T.Text (Either T.Text Result) -> | ||
M.Map T.Text (Either T.Text Result) -> | ||
(M.Map T.Text SpeedUp, ([T.Text], [T.Text])) | ||
compareDataResults prog a_path b_path a_data b_data = result | ||
where | ||
formatMissing = formatManyMissingData prog | ||
partition = E.partitionEithers . fmap sequence . M.toList | ||
(a_errors, a_data') = second M.fromList $ partition a_data | ||
(b_errors, b_data') = second M.fromList $ partition b_data | ||
missing = missingResults formatMissing a_path b_path a_data' b_data' | ||
exists = M.intersectionWith compareResult a_data' b_data' | ||
errors = a_errors ++ b_errors | ||
result = (exists, (errors, missing)) | ||
|
||
-- | Given two Maps containing program file paths as keys and values as datasets | ||
-- with results. Compare the results for each dataset in each program and | ||
-- return the errors in a tuple. | ||
compareBenchResults :: | ||
T.Text -> | ||
T.Text -> | ||
M.Map T.Text (M.Map T.Text (Either T.Text Result)) -> | ||
M.Map T.Text (M.Map T.Text (Either T.Text Result)) -> | ||
(M.Map T.Text (M.Map T.Text SpeedUp), ([T.Text], [T.Text])) | ||
compareBenchResults a_path b_path a_bench b_bench = (exists, errors_missing) | ||
where | ||
missing = missingResults formatManyMissingProg a_path b_path a_bench b_bench | ||
result = M.intersectionWithKey auxiliary a_bench b_bench | ||
auxiliary prog = compareDataResults prog a_path b_path | ||
exists = M.filter (not . null) $ fst <$> result | ||
errors_missing' = bimap concat concat . unzip . M.elems $ snd <$> result | ||
errors_missing = second (missing ++) errors_missing' | ||
|
||
-- | Formats memory usage such that it is human readable. If the memory usage | ||
-- is not significant an empty text is returned. | ||
memoryFormatter :: Colors -> T.Text -> Double -> T.Text | ||
memoryFormatter colors key value | ||
| value < 0.99 = memoryFormat $ okgreen colors | ||
| value > 1.01 = memoryFormat $ failing colors | ||
| otherwise = "" | ||
where | ||
memoryFormat c = T.pack $ printf "%s%4.2fx@%s%s" c value key endc' | ||
endc' = endc colors | ||
|
||
-- | Given a SpeedUp record the memory usage will be formatted to a colored | ||
-- human readable text. | ||
toMemoryText :: Colors -> SpeedUp -> T.Text | ||
toMemoryText colors data_result | ||
| T.null memory_text = "" | ||
| otherwise = " (mem: " <> memory_text <> ")" | ||
where | ||
memory_text = M.foldrWithKey formatFolder "" memory | ||
memory = memoryUsage data_result | ||
formatFolder key value lst = lst <> memoryFormatter colors key value | ||
|
||
-- | Given a text shorten it to a given length and add a suffix as the last | ||
-- word. | ||
shorten :: Int -> T.Text -> T.Text -> T.Text | ||
shorten c end string | ||
| T.length string > c = (T.unwords . init $ T.words shortened) <> " " <> end | ||
| otherwise = string | ||
where | ||
end_len = T.length end | ||
(shortened, _) = T.splitAt (c - end_len) string | ||
|
||
-- | Given a text add padding to the right of the text in form of spaces. | ||
rightPadding :: Int -> T.Text -> T.Text | ||
rightPadding c = T.pack . printf s | ||
where | ||
s = "%-" <> show c <> "s" | ||
|
||
-- | Given a SpeedUp record print the SpeedUp in a human readable manner. | ||
printSpeedUp :: Colors -> T.Text -> SpeedUp -> IO () | ||
printSpeedUp colors dataset data_result = do | ||
let color | ||
| significant data_result && speedup data_result > 1.01 = okgreen colors | ||
| significant data_result && speedup data_result < 0.99 = failing colors | ||
| otherwise = "" | ||
let short_dataset = rightPadding 64 . (<> ":") $ shorten 63 "[...]" dataset | ||
let memoryText = toMemoryText colors data_result | ||
let speedup' = speedup data_result | ||
let endc' = endc colors | ||
let format = " %s%s%10.2fx%s%s" | ||
putStrLn $ printf format short_dataset color speedup' endc' memoryText | ||
|
||
-- | Given a Map of SpeedUp records where the key is the program, print the | ||
-- SpeedUp in a human readable manner. | ||
printProgSpeedUps :: Colors -> T.Text -> M.Map T.Text SpeedUp -> IO () | ||
printProgSpeedUps colors prog bench_result = do | ||
putStrLn "" | ||
putStrLn $ printf "%s%s%s%s" (header colors) (bold colors) prog (endc colors) | ||
mapM_ (uncurry (printSpeedUp colors)) $ M.toList bench_result | ||
|
||
-- | Given a Map of programs with dataset speedups and relevant errors, print | ||
-- the errors and print the speedups in a human readable manner. | ||
printComparisons :: | ||
Colors -> | ||
M.Map T.Text (M.Map T.Text SpeedUp) -> | ||
([T.Text], [T.Text]) -> | ||
IO () | ||
printComparisons colors speedups (errors, missing) = do | ||
mapM_ (putStrLn . T.unpack) $ L.sort missing | ||
mapM_ (putStrLn . T.unpack) $ L.sort errors | ||
mapM_ (uncurry (printProgSpeedUps colors)) $ M.toList speedups | ||
|
||
-- | Run @futhark benchcmp@ | ||
main :: String -> [String] -> IO () | ||
main = mainWithOptions () [] "<file> <file>" f | ||
where | ||
f [a_path', b_path'] () = Just $ do | ||
let a_path = T.pack a_path' | ||
let b_path = T.pack b_path' | ||
a_either <- decodeFileBenchResultsMap a_path | ||
b_either <- decodeFileBenchResultsMap b_path | ||
|
||
isTty <- hSupportsANSI stdout | ||
|
||
let colors = | ||
if isTty | ||
then ttyColors | ||
else nonTtyColors | ||
|
||
let comparePrint = | ||
(uncurry (printComparisons colors) .) | ||
. compareBenchResults a_path b_path | ||
|
||
case (a_either, b_either) of | ||
(Left a, Left b) -> putStrLn . T.unpack $ (a <> "\n" <> b) | ||
(Left a, _) -> putStrLn . T.unpack $ a | ||
(_, Left b) -> putStrLn . T.unpack $ b | ||
(Right a, Right b) -> comparePrint a b | ||
f _ _ = Nothing |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters