sandwatch-0.1.1.0: src/MyLib.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
module MyLib where
import Data.Aeson
import Data.Aeson.Text
import qualified Data.ByteString as BS
import Data.Either
import Data.Int (Int64)
import qualified Data.Text.Lazy as T
import Data.Time
import GHC.Generics
import System.AtomicWrite.Writer.LazyText
import System.Directory (
XdgDirectory (XdgData),
createDirectoryIfMissing,
doesFileExist,
getXdgDirectory,
)
someFunc :: IO ()
someFunc = putStrLn "someFunc"
-- datatype
data Run = Run
{ tstamp :: !UTCTime -- when was this run?
, cmdline :: !T.Text -- what was the command?
, cwd :: !T.Text
, runTime :: !Double -- seconds of wall clock time
, cpuTime :: !Double -- cpu seconds
, cpuCycles :: !Int64 -- cpu cycles from rdtsc instruction
}
deriving (Show, Eq, Ord, Generic, ToJSON, FromJSON)
-- ugh, I wish there was an easier way, but whatever
data Runs = Runs
{ rs :: ![Run]
}
deriving (Show, Eq, Ord, Generic, ToJSON, FromJSON)
-- read and write data
readSandWatchData :: IO Runs
readSandWatchData = do
sandWatchDataFile <- getSandWatchFilePath
dataExists <- doesFileExist sandWatchDataFile
if not dataExists
then pure $ Runs []
else do
contents <- BS.readFile sandWatchDataFile
pure $ fromRight (Runs []) (eitherDecodeStrict contents) -- failed to parse json? you get an empty history!
writeSandWatchData :: Runs -> IO ()
writeSandWatchData rs' = do
sandWatchDataFile <- getSandWatchFilePath
atomicWriteFile sandWatchDataFile (encodeToLazyText rs')
getSandWatchFilePath :: IO FilePath
getSandWatchFilePath = do
sandWatchDir <- getXdgDirectory XdgData "sandwatch"
createDirectoryIfMissing True sandWatchDir -- is this a bad place to do this?
pure $ sandWatchDir <> "/runs"
-- analyze
{-
prefix match for estimate?
1. filter on cwd,
2. prefix match up to the first space
3. average the past matching runtimes
4. divide by five minutes
5. write out number of sandwiches estimated before running the command
-}
-- need to strip /home/shae/ and /Users/shae/ and whatever shows up on Windows?
-- whatever, do the thing and come back and clean up
filterByCWD :: Runs -> T.Text -> Runs
filterByCWD runs thiscwd = Runs $ filter (\r -> T.strip thiscwd == cwd r) (rs runs)
filterByFirstWord :: Runs -> T.Text -> Runs
filterByFirstWord runs thiscmdline = Runs $ filter (\r -> fstWord thiscmdline == fstWord (cmdline r)) (rs runs)
where
fstWord = fst . T.breakOn " "
-- "cabal build" for example
filterByFirstNWords :: Runs -> Int -> T.Text -> Runs
filterByFirstNWords runs n thiscmdline = Runs $ filter (\r -> fstWords n thiscmdline == fstWords n (cmdline r)) (rs runs)
where
fstWords n' c = take n' $ T.splitOn " " c
-- | expects historical runs, number of words to match, a Run that has cmdline and cwd populated
guessTime :: Int -> Runs -> Run -> Double
guessTime n pastruns thisrun = avgWallSeconds
where
runsInThisDir = filterByCWD pastruns (cwd thisrun)
runsMatchingNWords = filterByFirstNWords runsInThisDir n (cmdline thisrun)
wallTimes = runTime <$> rs runsMatchingNWords
avgWallSeconds = sum wallTimes / fromIntegral (length wallTimes)
-- cycles = cpuCycles <$> (rs runsMatchingNWords)
-- avgCycles = sum cycles /
-- would need to promote this Int64 to Integer for the math to work out?