packages feed

sandwatch (empty) → 0.1.1.0

raw patch · 8 files changed

+289/−0 lines, 8 filesdep +aesondep +atomic-writedep +basesetup-changed

Dependencies added: aeson, atomic-write, base, bytestring, criterion-measurement, directory, process, sandwatch, text, time

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for sandwatch++## 0.1.0.0 -- YYYY-mm-dd++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2020, Shae Erisson++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Shae Erisson nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Main.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Criterion.Measurement+import Criterion.Measurement.Types (Measured (..), nfAppIO)+import qualified Data.Text.Lazy as T+import Data.Time (getCurrentTime)+import MyLib+import Numeric+import System.Directory (getCurrentDirectory)+import System.Environment+import System.Process++-- | A sandwich takes five minutes according to my poll: https://twitter.com/shapr/status/1263950892744806402+sandwichSeconds :: Double+sandwichSeconds = 5 * 60++-- | How many words at the beginning of the command line to match?+wordsToMatch :: Int+wordsToMatch = 5++main :: IO ()+main = do+  history <- readSandWatchData+  args <- getArgs+  now <- getCurrentTime+  thiscwd <- getCurrentDirectory+  let thisRun = Run now (T.strip . T.pack $ unwords args) (T.pack thiscwd) 0 0 0 -- could I treat cmdline params as a Set?+  let guess = guessTime wordsToMatch history thisRun+  putStrLn $ "This command will probably take " <> formatFloat (guess / (5 * 60)) <> " sandwiches to complete."+  let bmark = nfAppIO callCommand (unwords args)+  (msr, _) <- measure bmark 1+  let thisRun' =+        thisRun+          { runTime = measTime msr,+            cpuTime = measCpuTime msr,+            cpuCycles = measCycles msr+          }+  writeSandWatchData $ Runs (thisRun' : rs history)+  putStrLn $ "Estimated time was " <> formatFloat (guess / (5 * 60)) <> " sandwiches, actual time was " <> formatFloat (measTime msr / sandwichSeconds) <> " sandwiches."+  where+    formatFloat f = showFFloat (Just 2) f ""
+ README.md view
@@ -0,0 +1,38 @@+# sandwatch+Remember how long commands take, tell me if I have enough time to make a sandwich?++This is inspired by [arbtt](https://arbtt.nomeata.de/#what), the shell built-in time, and the rust utility [tally](https://crates.io/crates/tally).++The name was suggested by [qu1j0t3](https://github.com/qu1j0t3/).++# Why?++Rather than telling me how long something has taken, I would like to know roughly how long this task will take, based on historical records.++Sandwatch records the current directory, the command line, and the runtime for a command.++When running a new command that matches the directory and the first two words of the command line, sandwatch reports expected completion time in sandwich units (five minutes?).++# How to use it?++It's a wrapper command. `sandwatch cabal build` or `sandwatch make` or whatever you like. Perhaps one day this will become a shell builtin written in Rust.++# How to install it?++I suggest using [ghcup](https://www.haskell.org/ghcup/) to install the Haskell compiler and then `cabal build` in your clone of this git repo.++# How does it work?++Sandwatch creates a json file holding entries recording the working directory, the command line, and the execution time.++When a command matches in the same directory, execution from previous runs is average together to give you a rough idea as to whether you have time to make a sandwich or not!++# Tell me the root problem?++I don't have insight into the progress of a running program. I wish all programs would dump some number of percent complete events. I could use that to estimate completion time from start time.++# Ideas++* Is there a good way to turn command line arguments into a set? For example, `ls -o -a` is the same as `ls -a -o` but the strings are different. How could they be the same?+* Is there some good way to automatically figure out how much of a command line to compare for equality?+* How could I compare command lines for equality across directories? While `make` does different things in different directories, `apt install` is always the same. How do you decide?
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ sandwatch.cabal view
@@ -0,0 +1,65 @@+cabal-version:      >=1.10+category:           Command Line+name:               sandwatch+synopsis:           record historical command runtimes for later prediction+description:        track historical runtimes for this directory and command, predict if there's time for a sandwich on this run+version:            0.1.1.0+license:            BSD3+license-file:       LICENSE+author:             Shae Erisson+maintainer:         shae@scannedinavian.com+build-type:         Simple+extra-source-files:+  CHANGELOG.md+  README.md++library+  exposed-modules:  MyLib+  ghc-options:      -Wall -O2+  build-depends:+      aeson                  >=1.4.7.1 && < 2.2+    , atomic-write >= 0.2.0 && < 0.3+    , base                   >=4.14    && <4.20+    , bytestring >= 0.11 && < 0.12+    , criterion-measurement >= 0.2 && < 0.3+    , directory >= 1.3 && < 1.5+    , process >= 1.6 && < 1.7+    , text >= 2.0 && < 2.1+    , time >= 1.12 && < 1.13++  hs-source-dirs:   src+  default-language: Haskell2010++executable sandwatch+  main-is:          Main.hs+  ghc-options:      -Wall -O2+  build-depends:+      aeson                  >=1.4.7.1 && < 2.2+    , atomic-write >= 0.2.0 && < 0.3+    , base                   >=4.14    && <4.20+    , bytestring >= 0.11 && < 0.12+    , criterion-measurement >= 0.2 && < 0.3+    , directory >= 1.3 && < 1.5+    , process >= 1.6 && < 1.7+    , sandwatch+    , text >= 2.0 && < 2.1+    , time >= 1.12 && < 1.13++  default-language: Haskell2010++test-suite sandwatch-test+  default-language: Haskell2010+  type:             exitcode-stdio-1.0+  hs-source-dirs:   test+  main-is:          MyLibTest.hs+  ghc-options:      -Wall -O2+  build-depends:+      aeson                  >=1.4.7.1 && < 2.2+    , atomic-write >= 0.2.0 && < 0.3+    , base                   >=4.14    && <4.20+    , bytestring >= 0.11 && < 0.12+    , criterion-measurement >= 0.2 && < 0.3+    , directory >= 1.3 && < 1.5+    , process >= 1.6 && < 1.7+    , text >= 2.0 && < 2.1+    , time >= 1.12 && < 1.13
+ src/MyLib.hs view
@@ -0,0 +1,102 @@+{-# 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?
+ test/MyLibTest.hs view
@@ -0,0 +1,4 @@+module Main (main) where++main :: IO ()+main = putStrLn "Test suite not yet implemented."