Skip to content

Commit ba82465

Browse files
committed
Memory/time benchmarks
* added memory/time benchmarks and a command line 'bench.py' utility for comparing benchmarks over multiple commits * added a couple of parsers to the tests and fixed spans on parsing blocks * added a 'Reversed' data type for efficient appending, for later use in the parser
1 parent 19ca6df commit ba82465

File tree

18 files changed

+451
-98
lines changed

18 files changed

+451
-98
lines changed

bench.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
import sys
5+
import subprocess
6+
import tabulate
7+
import argparse
8+
9+
def merge(dict1, dict2, def1, def2, func):
10+
"""Merge two nested dictionaries, using default values when it makes sense"""
11+
assert isinstance(dict1, dict)
12+
assert isinstance(dict2, dict)
13+
14+
toReturn = {}
15+
keys1 = set(dict1.keys())
16+
keys2 = set(dict2.keys())
17+
18+
for key in keys1 | keys2: # change this to |
19+
val1 = dict1.get(key, None)
20+
val2 = dict2.get(key, None)
21+
22+
if isinstance(val1,dict) or isinstance(val2,dict):
23+
toReturn[key] = merge(val1 or {}, val2 or {}, def1, def2, func)
24+
else:
25+
toReturn[key] = func(val1 or def1, val2 or def2)
26+
27+
return toReturn
28+
29+
30+
def flattenListDict(d, indent=0):
31+
"""Flatten a nested dictionary into a list of lists representing a table"""
32+
assert isinstance(d, dict)
33+
result = []
34+
for k,v in d.items():
35+
assert isinstance(k, str)
36+
if isinstance(v, list):
37+
first = None
38+
row = []
39+
for entry in v:
40+
if entry:
41+
if first:
42+
percentDiff = 100 * (float(entry) - first) / first
43+
color = '\033[92m' if percentDiff > -1.0 else '\033[91m'
44+
row.append("%s%2.1f%s" % (color, percentDiff, '%\033[0m'))
45+
else:
46+
first = float(entry)
47+
row.append(entry)
48+
else:
49+
row.append(entry)
50+
51+
result.append([ '.' * indent + k ] + row)
52+
elif isinstance(v, dict):
53+
result.append([ '.' * indent + k ])
54+
result.extend(flattenListDict(v, indent + 2))
55+
else:
56+
raise "List dict can only contain lists or other list dicts"
57+
return result
58+
59+
# Currently not used...
60+
def fmtSize(num):
61+
for unit in ['','KB','MB','GB','TB','PB','EB','ZB']:
62+
if abs(num) < 1024.0:
63+
return "%3.1f%s" % (num, unit)
64+
num /= 1024.0
65+
return "%.1f%s%s" % (num, 'YB', suffix)
66+
67+
68+
if __name__ == "__main__":
69+
# Argument parser
70+
parser = argparse.ArgumentParser()
71+
parser.add_argument('--folder', default='.', type=str, help='benchmark folder to analyze')
72+
parser.add_argument('--last', nargs='?', default=5, type=int, help='include benchmarks for the last "n" commits')
73+
parser.add_argument('--exact', nargs='*', default=[], type=str, help='include benchmarks for specific commits')
74+
parsed = parser.parse_args(sys.argv[1:])
75+
76+
# Commits
77+
commits = ["WIP", "HEAD"]
78+
if parsed.last:
79+
commits.extend([ "HEAD~" + str(i) for i in range(1,parsed.last) ])
80+
if parsed.exact:
81+
commits.extend([ str(commit) for commit in parsed.exact ])
82+
83+
# Sanitized commits
84+
sanitized = ["WIP"]
85+
for commit in commits[1:]:
86+
try:
87+
c = subprocess.check_output(["git", "rev-parse", commit]).decode("utf-8").strip()
88+
sanitized.append(c)
89+
except:
90+
print('Invalid commit "' + commit + '"')
91+
92+
# Load the JSONs
93+
datas = []
94+
for sane in sanitized:
95+
try:
96+
with open(parsed.folder + '/' + sane + '.json') as json_data:
97+
datas.append(json.load(json_data))
98+
except:
99+
print('Could not read file for "' + sane + '.json"')
100+
datas.append({})
101+
102+
# Aggregate the output
103+
aggregated = {}
104+
n = 0
105+
for data in datas:
106+
aggregated = merge(aggregated, data, n * [ None ], None, lambda xs, x: xs + [x])
107+
n += 1
108+
109+
# Convert to a table
110+
print(tabulate.tabulate(flattenListDict(aggregated), [ '' ] + commits))
111+
112+
113+
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
import Weigh
4+
5+
import Control.Monad (filterM)
6+
import Data.Foldable (traverse_)
7+
import GHC.Exts (fromString)
8+
9+
import Language.Rust.Parser (parseSourceFile')
10+
11+
import System.Directory (getCurrentDirectory, listDirectory, createDirectoryIfMissing, doesFileExist)
12+
import System.FilePath ((</>), (<.>), takeFileName)
13+
import System.Process (proc, readCreateProcess)
14+
15+
import Data.Aeson
16+
import qualified Data.ByteString.Lazy as BL
17+
18+
-- TODO:
19+
-- Only allocation and GCs seem to be really reproducible. Live and max sometimes are 0.
20+
21+
main :: IO ()
22+
main = do
23+
-- Open the output log file
24+
status <- readCreateProcess (proc "git" ["status", "--porcelain"]) ""
25+
logFileName <- case status of
26+
"" -> init <$> readCreateProcess (proc "git" ["rev-parse", "HEAD"]) ""
27+
_ -> pure "WIP"
28+
29+
-- Get the test cases
30+
workingDirectory <- getCurrentDirectory
31+
let sampleSources = workingDirectory </> "sample-sources"
32+
entries <- map (sampleSources </>) <$> listDirectory sampleSources
33+
files <- filterM doesFileExist entries
34+
35+
-- Run 'weigh' tests
36+
let weigh = setColumns [ Case, Max, Allocated, GCs, Live ] >> traverse_ (\f -> io (takeFileName f) parseSourceFile' f) files
37+
mainWith weigh
38+
(wr, _) <- weighResults weigh
39+
let results = object [ case maybeErr of
40+
Nothing -> key .= object [ "allocated" .= weightAllocatedBytes weight
41+
-- , "max" .= weightMaxBytes w
42+
-- , "live" .= weightLiveBytes w
43+
-- , "GCs" .= weightGCs w
44+
]
45+
Just err -> key .= String (fromString err)
46+
| (weight, maybeErr) <- wr
47+
, let key = fromString (weightLabel weight)
48+
]
49+
50+
-- Save the output to JSON
51+
createDirectoryIfMissing False (workingDirectory </> "allocations")
52+
let logFile = workingDirectory </> "allocations" </> logFileName <.> "json"
53+
logFile `BL.writeFile` encode results
54+

benchmarks/timing-benchmarks/Main.hs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
import Criterion
4+
import Criterion.Types (anMean, reportAnalysis)
5+
import Statistics.Resampling.Bootstrap (Estimate(..))
6+
7+
import Control.Monad (filterM)
8+
import Data.Traversable (for)
9+
import GHC.Exts (fromString)
10+
11+
import Language.Rust.Parser (parseSourceFile')
12+
13+
import System.Directory (getCurrentDirectory, listDirectory, createDirectoryIfMissing, doesFileExist)
14+
import System.FilePath ((</>), (<.>), takeFileName)
15+
import System.Process (proc, readCreateProcess)
16+
17+
import Data.Aeson
18+
import qualified Data.ByteString.Lazy as BL
19+
20+
main :: IO ()
21+
main = do
22+
-- Open the output log file
23+
status <- readCreateProcess (proc "git" ["status", "--porcelain"]) ""
24+
logFileName <- case status of
25+
"" -> init <$> readCreateProcess (proc "git" ["rev-parse", "HEAD"]) ""
26+
_ -> pure "WIP"
27+
28+
-- Get the test cases
29+
workingDirectory <- getCurrentDirectory
30+
let sampleSources = workingDirectory </> "sample-sources"
31+
entries <- map (sampleSources </>) <$> listDirectory sampleSources
32+
files <- filterM doesFileExist entries
33+
34+
-- Run 'criterion' tests
35+
reports <- for files $ \f -> do
36+
let name = takeFileName f
37+
putStrLn name
38+
bnch <- benchmark' (nfIO (parseSourceFile' f))
39+
pure (name, bnch)
40+
let results = object [ fromString name .= object [ "mean" .= m
41+
, "lower bound" .= l
42+
, "upper bound" .= u
43+
]
44+
| (name,report) <- reports
45+
, let Estimate m l u _ = anMean (reportAnalysis report)
46+
]
47+
48+
-- Save the output to JSON
49+
createDirectoryIfMissing False (workingDirectory </> "timings")
50+
let logFile = workingDirectory </> "timings" </> logFileName <.> "json"
51+
logFile `BL.writeFile` encode results
52+

language-rust.cabal

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ library
4646

4747
other-modules: Language.Rust.Parser.Internal
4848
Language.Rust.Parser.Literals
49+
Language.Rust.Parser.Reversed
4950
Language.Rust.Pretty.Resolve
5051
Language.Rust.Syntax.AST
5152
Language.Rust.Syntax.Ident
@@ -63,6 +64,7 @@ library
6364
, wl-pprint-annotated >=0.1.0.0 && <0.2.0.0
6465
, transformers >=0.5 && <0.6
6566
, array >=0.5 && <0.6
67+
, deepseq >=1.4.2.0
6668

6769
if flag(useByteStrings)
6870
build-depends: utf8-string >=1.0
@@ -85,8 +87,6 @@ test-suite unit-tests
8587
type: exitcode-stdio-1.0
8688
default-language: Haskell2010
8789
build-depends: base >=4.9 && <5.0
88-
, Cabal >= 1.10.0
89-
, transformers >=0.5 && <0.6
9090
, HUnit >=1.5.0.0
9191
, wl-pprint-annotated >=0.1.0.0 && <0.2.0.0
9292
, test-framework >=0.8.0
@@ -102,15 +102,45 @@ test-suite rustc-tests
102102
type: exitcode-stdio-1.0
103103
default-language: Haskell2010
104104
build-depends: base >=4.9 && <5.0
105-
, Cabal >= 1.10.0
106105
, process >= 1.3
107106
, bytestring >=0.10
108107
, aeson >= 1.0.0.0
109108
, directory >= 1.3.0.0
110109
, filepath >= 1.4.0.0
111-
, transformers >=0.5 && <0.6
112110
, test-framework >=0.8.0
113111
, vector >=0.10.0
114112
, text >=1.2.0
115113
, unordered-containers >= 0.2.7
116114
, language-rust
115+
116+
benchmark timing-benchmarks
117+
hs-source-dirs: benchmarks/timing-benchmarks
118+
ghc-options: -Wall
119+
main-is: Main.hs
120+
type: exitcode-stdio-1.0
121+
default-language: Haskell2010
122+
build-depends: base >=4.9 && <5.0
123+
, process >= 1.3
124+
, bytestring >=0.10
125+
, directory >= 1.3.0.0
126+
, filepath >= 1.4.0.0
127+
, language-rust
128+
, criterion >=1.1.1.0
129+
, statistics
130+
, aeson >= 1.0.0.0
131+
132+
benchmark allocation-benchmarks
133+
hs-source-dirs: benchmarks/allocation-benchmarks
134+
ghc-options: -Wall
135+
main-is: Main.hs
136+
type: exitcode-stdio-1.0
137+
default-language: Haskell2010
138+
build-depends: base >=4.9 && <5.0
139+
, process >= 1.3
140+
, bytestring >=0.10
141+
, directory >= 1.3.0.0
142+
, filepath >= 1.4.0.0
143+
, language-rust
144+
, weigh >=0.0.4
145+
, aeson >= 1.0.0.0
146+

sample-sources/items.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ mod bar {
1616

1717
extern { }
1818
extern "C" {
19-
fn foo(x: int) -> int;
19+
fn foo<T>(x: int) -> int;
2020
static x: int;
2121
static mut x: *mut int;
2222
}

src/Language/Rust/Data/Position.hs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Portability : portable
99
1010
Everything to do with describing a position or a contiguous region in a file.
1111
-}
12-
{-# LANGUAGE DeriveDataTypeable, DeriveGeneric, CPP #-}
12+
{-# LANGUAGE DeriveDataTypeable, DeriveGeneric, CPP, DeriveAnyClass #-}
1313

1414
module Language.Rust.Data.Position (
1515
-- * Positions in files
@@ -21,6 +21,7 @@ module Language.Rust.Data.Position (
2121
import GHC.Generics (Generic)
2222
import Data.Data (Data)
2323
import Data.Typeable (Typeable)
24+
import Control.DeepSeq (NFData)
2425

2526
import Data.Ord (comparing)
2627
import Data.List (maximumBy, minimumBy)
@@ -37,7 +38,7 @@ data Position = Position {
3738
col :: {-# UNPACK #-} !Int -- ^ column in the source file.
3839
}
3940
| NoPosition
40-
deriving (Eq, Show, Data, Typeable, Generic)
41+
deriving (Eq, Show, Data, Typeable, Generic, NFData)
4142

4243
-- | Pretty print a 'Position'
4344
prettyPosition :: Position -> String
@@ -98,7 +99,7 @@ data Span = Span {
9899
#else
99100
lo, hi :: !Position
100101
#endif
101-
} deriving (Eq, Show, Data, Typeable, Generic)
102+
} deriving (Eq, Show, Data, Typeable, Generic, NFData)
102103

103104
-- | Check if a span is a subset of another span
104105
subsetOf :: Span -> Span -> Bool
@@ -121,7 +122,7 @@ prettySpan :: Span -> String
121122
prettySpan (Span lo' hi') = show lo' ++ " - " ++ show hi'
122123

123124
-- | A "tagging" of something with a 'Span' that describes its extent.
124-
data Spanned a = Spanned { unspan :: a, span :: {-# UNPACK #-} !Span } deriving (Data, Typeable, Generic)
125+
data Spanned a = Spanned { unspan :: a, span :: {-# UNPACK #-} !Span } deriving (Data, Typeable, Generic, NFData)
125126

126127
instance Functor Spanned where
127128
fmap f (Spanned x s) = Spanned (f x) s

src/Language/Rust/Parser.hs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ sourceFile :: SourceFile Span
2323

2424
module Language.Rust.Parser (
2525
-- * Parsing
26-
parse, parse', Parse(..), P, execParser, initPos, Span,
26+
parse, parse', parseSourceFile', Parse(..), P, execParser, initPos, Span,
2727
-- * Lexing
2828
lexToken, lexNonSpace, lexTokens, translateLit,
2929
-- * Input stream
@@ -55,6 +55,10 @@ parse' is = case execParser parser is initPos of
5555
Left (pos, msg) -> throw (ParseFail pos msg)
5656
Right x -> x
5757

58+
-- | Given a path pointing to a Rust source file, read that file and parse it into a 'SourceFile'
59+
parseSourceFile' :: FilePath -> IO (SourceFile Span)
60+
parseSourceFile' fileName = parse' <$> readInputStream fileName
61+
5862
-- | Exceptions that occur during parsing
5963
data ParseFail = ParseFail Position String deriving (Eq, Typeable)
6064

@@ -70,6 +74,7 @@ class Parse a where
7074

7175
instance Parse (Lit Span) where parser = parseLit
7276
instance Parse (Attribute Span) where parser = parseAttr
77+
instance Parse (Arg Span) where parser = parseArg
7378
instance Parse (Ty Span) where parser = parseTy
7479
instance Parse (Pat Span) where parser = parsePat
7580
instance Parse (Expr Span) where parser = parseExpr

0 commit comments

Comments
 (0)