graft-0.1.0.0: bench/Fixtures.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
-- Workloads and observations shared by timing and residency runs.
module Fixtures
( Kind (..), Prepared (..), PreparedCursor (..), Named (..), Numbered (..), Grouped (..)
, wideSource, wideTree, deepTree, chainTree, chainSource, documentTree, documentTreeAgain, documentSource
, forceElement, checked, selected, cached, cursorEnd, siblingSum
, buildTokens, buildMonadic, buildReuse, buildWrapped, editMany, snapshots, Scope (..), renameTree, renamePasses, renameVisited
, checkFixtures
) where
import Control.DeepSeq (NFData (..))
import Control.Exception (evaluate)
import Control.Monad.Trans.State.Strict (modify', runState)
import Data.Hashable (Hashable (..))
import Control.Monad (unless)
import qualified Data.List as List
import Data.Text (Text)
import qualified Data.Text as Text
import Graft
import qualified Graft.Ast as Ast
import qualified Graft.Build as Build
import qualified Graft.Builder as Builder
import qualified Graft.Cursor as Cursor
import qualified Graft.Edit as Edit
import qualified Graft.Green as Green
import qualified Graft.Range as Range
data Kind = Root | Group | Name | Number | Trivia
deriving (Eq, Show, Enum)
instance Hashable Kind where
hashWithSalt salt = hashWithSalt salt . fromEnum
instance NFData Kind where
rnf kind = kind `seq` ()
-- These wrappers force fixtures before the timer starts.
newtype Prepared = Prepared { preparedTree :: GreenElement Kind }
instance NFData Prepared where
rnf (Prepared tree) = rnf tree
newtype PreparedCursor = PreparedCursor { preparedCursor :: Cursor Kind }
instance NFData PreparedCursor where
rnf (PreparedCursor cursor) = rnf cursor
forceElement :: GreenElement Kind -> ()
forceElement = rnf
newtype Named = Named (Cursor Kind)
instance AstView Named where
type AstViewKind Named = Kind
fromCursor = Ast.matchToken (== Name) Named
toCursor (Named cursor) = cursor
instance AstToken Named
newtype Numbered = Numbered (Cursor Kind)
instance AstView Numbered where
type AstViewKind Numbered = Kind
fromCursor = Ast.matchToken (== Number) Numbered
toCursor (Numbered cursor) = cursor
instance AstToken Numbered
newtype Grouped = Grouped (Cursor Kind)
instance AstView Grouped where
type AstViewKind Grouped = Kind
fromCursor = Ast.matchNode (== Group) Grouped
toCursor (Grouped cursor) = cursor
instance AstNode Grouped
wideSource :: Int -> [Text]
wideSource count = [Text.pack ("名" ++ show index ++ " ") | index <- [1 .. count]]
wideTree :: [Text] -> GreenNode Kind
wideTree = Green.node Root . wideElements
wideElements :: [Text] -> [GreenElement Kind]
wideElements [] = []
wideElements [text] = [Token (Green.token Number text)]
wideElements (text : rest) = Token (Green.token Name text) : wideElements rest
deepTree :: Int -> GreenNode Kind
deepTree depth = List.foldl' (\node _ -> Green.node Group [Node node]) leaf [1 .. depth]
where leaf = Green.node Group [Token (Green.token Number "001")]
-- A left-nested operator chain such as @((0 + 1) + 2) + 3@: both deep and
-- wide, so whole-tree traversals pay for every level on every token.
chainTree :: Int -> GreenNode Kind
chainTree count = List.foldl' step leaf [1 .. count]
where
leaf = Green.node Group [Token (Green.token Number "0")]
step left index = Green.node Group
[Node left, Token (Green.token Trivia " + "), Token (Green.token Number (Text.pack (show index)))]
chainSource :: Int -> Text
chainSource count = Text.concat ("0" : [Text.pack (" + " ++ show index) | index <- [1 .. count]])
lineText :: Int -> [Text]
lineText index = [Text.pack ("名" ++ show index), " = ", "001", " -- keep", "\r\n"]
documentSource :: Int -> Text
documentSource count = Text.concat (concatMap lineText [1 .. count])
documentTree :: Int -> GreenNode Kind
documentTree count = Green.node Root [Node (line index) | index <- [1 .. count]]
where
line index = Green.node Group (zipWith (\kind text -> Token (Green.token kind text))
[Name, Trivia, Number, Trivia, Trivia] (lineText index))
-- The same document built again, kept apart from documentTree by the
-- optimiser, so that the two are equal but not the same object.
documentTreeAgain :: Int -> GreenNode Kind
documentTreeAgain = documentTree
{-# NOINLINE documentTreeAgain #-}
checked :: Show err => Either err a -> a
checked = either (error . show) id
selected :: Maybe a -> a
selected = maybe (error "benchmark fixture has no selected element") id
cached :: GreenNode Kind -> TextLength
cached = Green.nodeLength
cursorEnd :: Cursor Kind -> TextLength
cursorEnd = Range.end . Cursor.range
-- A strict consumer: each adjacent sibling and range contributes to the result.
siblingSum :: Cursor Kind -> TextLength
siblingSum root = go 0 (Cursor.firstChild root)
where
go total Nothing = total
go total (Just cursor) = let next = total + cursorEnd cursor
in next `seq` go next (Cursor.nextSibling cursor)
buildTokens :: [Text] -> GreenNode Kind
buildTokens texts = checked (Builder.finish (checked (Builder.finishNode emitted)))
where
emitted = go (Builder.startNode Root Builder.empty) texts
go builder [] = builder
go builder [text] = Builder.token Number text builder
go builder (text : rest) = let next = Builder.token Name text builder
in next `seq` go next rest
buildMonadic :: [Text] -> GreenNode Kind
buildMonadic texts = checked (Build.exec (Build.node Root (go texts)))
where
go [] = pure ()
go [text] = Build.token Number text
go (text : rest) = Build.token Name text >> go rest
buildReuse :: GreenNode Kind -> GreenNode Kind
buildReuse node = checked (Builder.finish (checked (Builder.finishNode emitted)))
where emitted = List.foldl' (flip Builder.element) (Builder.startNode Root Builder.empty) (Green.children node)
buildWrapped :: GreenNode Kind -> GreenNode Kind
buildWrapped node = checked (Builder.finish closed)
where
opened = Builder.startNode Root Builder.empty
marker = Builder.checkpoint opened
emitted = List.foldl' (flip Builder.element) opened (Green.children node)
wrapped = checked (Builder.startNodeAt marker Group emitted)
closed = checked (Builder.finishNode (checked (Builder.finishNode wrapped)))
-- Each edit depends on the preceding snapshot. Distinct text prevents no-op
-- updates. Force the cache on every step without walking shared descendants.
editMany :: Int -> Path -> GreenElement Kind -> GreenElement Kind
editMany count path root = List.foldl' (editAt path) root [1 .. count]
-- One replacement at a path, through a plan of one edit.
editAt :: Path -> GreenElement Kind -> Int -> GreenElement Kind
editAt path previous index =
let focus = checked (Cursor.at path (Cursor.fromGreen previous))
updated = checked (Edit.apply previous [Edit.replace focus (Token (Green.token Number (Text.pack (show index))))])
in Green.length updated `seq` updated
-- What a renaming rewrite reads before renaming each token, as a rename that
-- checks scopes would: nothing, the ranges of its ancestors, or the ranges
-- of its ancestors that cast to Grouped.
data Scope = Everywhere | InAncestors | InGroups
-- Rename every Name token in scope with one rewrite, as the Graft.Edit
-- example does.
renameTree :: Scope -> GreenElement Kind -> GreenElement Kind
renameTree scope = Edit.rewrite (\() cursor -> renaming scope cursor) ()
renaming :: Scope -> Cursor Kind -> Change () Kind
renaming scope cursor
| not (Cursor.isToken cursor) = Descend
| inScope && Cursor.kind cursor == Name = Replace (Token (Green.token Name "renamed"))
| otherwise = Skip
where
inScope = case scope of
Everywhere -> True
InAncestors -> ends (Cursor.ancestors cursor) > 0
InGroups -> ends (map toCursor (Ast.ancestors cursor :: [Grouped])) > 0
ends = List.foldl' (\total ancestor -> total + cursorEnd ancestor) 0
-- Every cursor the renaming rewrite visits, with its result.
renameVisited :: Scope -> GreenElement Kind -> ([Cursor Kind], GreenElement Kind)
renameVisited scope input =
let (result, visited) = runState (Edit.rewriteM step () input) []
step () cursor = modify' (cursor :) >> pure (renaming scope cursor)
in (visited, result)
-- The renaming rewrite repeated, each pass on the result of the previous.
renamePasses :: Int -> GreenElement Kind -> GreenElement Kind
renamePasses count input = List.foldl' pass input [1 .. count]
where
pass tree _ = let next = renameTree Everywhere tree in Green.length next `seq` next
snapshots :: Int -> Path -> GreenElement Kind -> [GreenElement Kind]
snapshots count path root = scanl (editAt path) root [1 .. count]
-- Independent source checks guard the workload, not timing thresholds.
checkFixtures :: IO ()
checkFixtures = do
mapM_ checkWide [100, 1000, 10000]
mapM_ checkDeep [100, 1000, 10000]
mapM_ checkChain [100, 1000, 10000]
let document = Node (documentTree 1000)
before = documentSource 1000
path = [499, 2]
cursor = checked (Cursor.at path (Cursor.fromGreen document))
updated = editMany 100 path document
renamed = renameTree Everywhere document
expected = Text.concat [documentSource 499, Text.concat
["名500", " = ", "100", " -- keep", "\r\n"],
Text.concat (concatMap lineText [501 .. 1000])]
assert "document source" (Green.render document == before)
assert "repeated exact edits" (Green.render updated == expected)
assert "one edit and repeated edits agree"
(checked (Edit.apply document [Edit.replace cursor (Token (Green.token Number "100"))]) == updated)
assert "original snapshot preserved" (Green.render document == before)
assert "typed groups" (length (Ast.children (Cursor.fromGreen document) :: [Grouped]) == 1000)
assert "renamed source" (Green.render renamed == Text.concat (concatMap renamedLine [1 .. 1000 :: Int]))
assert "scoped renames" (all (\scope -> renameTree scope document == renamed) [InAncestors, InGroups])
assert "rewrite visits every element" (length (fst (renameVisited Everywhere document)) == 1 + 1000 * 6)
assert "history count" (length (snapshots 100 path document) == 101)
putStrLn "Benchmark fixtures and observations: PASS"
where
assert label valid = unless valid (error ("benchmark check failed: " ++ label))
renamedLine index = "renamed" : drop 1 (lineText index)
checkWide count = do
let texts = wideSource count
node = wideTree texts
root = Cursor.fromGreen (Node node)
insertion = Token (Green.token Number "NEW")
inserted = checked (Green.spliceChildren (count `div` 2) (count `div` 2) [insertion] node)
_ <- evaluate (forceElement (Node node))
assert "wide exact source" (Green.renderNode node == Text.concat texts)
assert "builder tokens" (buildTokens texts == node)
assert "builder reuse" (buildReuse node == node)
assert "monadic builder" (buildMonadic texts == node)
assert "checkpoint source" (Green.renderNode (buildWrapped node) == Text.concat texts)
assert "wide UTF-8 cache" (Green.nodeLength node == sum
[4 + fromIntegral (length (show index)) | index <- [1 .. count]])
assert "last offset" (cursorEnd (checked (Cursor.childAt (count - 1) root)) == Green.nodeLength node)
assert "sibling observation" (siblingSum root == sum (map cursorEnd (Cursor.children root)))
assert "typed last token" (cursorEnd (toCursor (selected (Ast.token root :: Maybe Numbered))) == Green.nodeLength node)
assert "splice exact source" (Green.renderNode inserted == Text.concat
[Text.concat (take (count `div` 2) texts), "NEW", Text.concat (drop (count `div` 2) texts)])
checkChain count = do
let node = chainTree count
root = Cursor.fromGreen (Node node)
_ <- evaluate (forceElement (Node node))
assert "chain source" (Green.renderNode node == chainSource count)
assert "chain tokens" (length (Cursor.descendantTokens root) == 2 * count + 1)
assert "chain first offset" (fmap cursorEnd (singleToken (Cursor.tokenAt 0 root)) == Just 1)
singleToken (SingleToken cursor) = Just cursor
singleToken _ = Nothing
checkDeep depth = do
let node = deepTree depth
root = Cursor.fromGreen (Node node)
path = replicate (depth + 1) 0
leaf = checked (Cursor.at path root)
_ <- evaluate (forceElement (Node node))
assert "deep source" (Green.renderNode node == "001")
assert "deep path" (Cursor.path leaf == path)
assert "deep cached range" (cursorEnd leaf == 3)
assert "deep edit" (Green.render (editMany 2 path (Node node)) == "2")