-
-
Notifications
You must be signed in to change notification settings - Fork 392
Add RangeMap
for unified "in-range" filtering
#3343
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
d4d312e
3fab3ac
3347743
2d13a0c
288cbf1
eda7f65
ad02e7c
d481385
527e5fd
79f7446
492f2e6
7998b9c
f2d1536
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
-- A benchmark comparing the performance characteristics of list-based | ||
-- vs RangeMap-based "in-range filtering" approaches | ||
module Main (main) where | ||
|
||
import Control.DeepSeq (force) | ||
import Control.Exception (evaluate) | ||
import Control.Monad (replicateM) | ||
import qualified Criterion | ||
import qualified Criterion.Main | ||
import Data.Random (RVar) | ||
import qualified Data.Random as Fu | ||
import qualified Ide.Plugin.RangeMap as RangeMap | ||
import Language.LSP.Types (Position (..), Range (..), UInt, | ||
isSubrangeOf) | ||
import qualified System.Random.Stateful as Random | ||
|
||
|
||
genRangeList :: Int -> RVar [Range] | ||
genRangeList n = replicateM n genRange | ||
|
||
genRange :: RVar Range | ||
genRange = do | ||
x1 <- genPosition | ||
delta <- genRangeLength | ||
let x2 = x1 { _character = _character x1 + delta } | ||
pure $ Range x1 x2 | ||
where | ||
genRangeLength :: RVar UInt | ||
genRangeLength = fromInteger <$> Fu.uniform 5 50 | ||
|
||
genPosition :: RVar Position | ||
genPosition = Position | ||
<$> (fromInteger <$> Fu.uniform 0 10000) | ||
<*> (fromInteger <$> Fu.uniform 0 150) | ||
|
||
filterRangeList :: Range -> [Range] -> [Range] | ||
filterRangeList r = filter (isSubrangeOf r) | ||
|
||
main :: IO () | ||
main = do | ||
rangeLists@[rangeList100, rangeList1000, rangeList10000] | ||
<- traverse (Fu.sampleFrom Random.globalStdGen . genRangeList) [100, 1000, 10000] | ||
[rangeMap100, rangeMap1000, rangeMap10000] <- evaluate $ force $ map (RangeMap.fromList id) rangeLists | ||
targetRange <- Fu.sampleFrom Random.globalStdGen genRange | ||
Criterion.Main.defaultMain | ||
[ Criterion.bgroup "List" | ||
[ Criterion.bench "Size 100" $ Criterion.nf (filterRangeList targetRange) rangeList100 | ||
, Criterion.bench "Size 1000" $ Criterion.nf (filterRangeList targetRange) rangeList1000 | ||
, Criterion.bench "Size 10000" $ Criterion.nf (filterRangeList targetRange) rangeList10000 | ||
] | ||
, Criterion.bgroup "RangeMap" | ||
[ Criterion.bench "Size 100" $ Criterion.nf (RangeMap.filterByRange targetRange) rangeMap100 | ||
, Criterion.bench "Size 1000" $ Criterion.nf (RangeMap.filterByRange targetRange) rangeMap1000 | ||
, Criterion.bench "Size 10000" $ Criterion.nf (RangeMap.filterByRange targetRange) rangeMap10000 | ||
] | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
{-# LANGUAGE CPP #-} | ||
{-# LANGUAGE DeriveFoldable #-} | ||
{-# LANGUAGE DeriveFunctor #-} | ||
{-# LANGUAGE DeriveTraversable #-} | ||
{-# LANGUAGE DerivingStrategies #-} | ||
{-# LANGUAGE GeneralizedNewtypeDeriving #-} | ||
|
||
-- | A map that allows fast \"in-range\" filtering. 'RangeMap' is meant | ||
-- to be constructed once and cached as part of a Shake rule. If | ||
-- not, the map will be rebuilt upon each invocation, yielding slower | ||
-- results compared to the list-based approach! | ||
-- | ||
-- Note that 'RangeMap' falls back to the list-based approach if | ||
-- `use-fingertree` flag of `hls-plugin-api` is set to false. | ||
module Ide.Plugin.RangeMap | ||
( RangeMap(..), | ||
fromList, | ||
fromList', | ||
filterByRange, | ||
) where | ||
|
||
import Data.Bifunctor (first) | ||
import Data.Foldable (foldl') | ||
import Development.IDE.Graph.Classes (NFData) | ||
import Language.LSP.Types (Position, | ||
Range (Range), | ||
isSubrangeOf) | ||
#ifdef USE_FINGERTREE | ||
import qualified HaskellWorks.Data.IntervalMap.FingerTree as IM | ||
#endif | ||
|
||
-- | A map from code ranges to values. | ||
#ifdef USE_FINGERTREE | ||
newtype RangeMap a = RangeMap | ||
{ unRangeMap :: IM.IntervalMap Position a | ||
-- ^ 'IM.Interval' of 'Position' corresponds to a 'Range' | ||
} | ||
deriving newtype (NFData, Semigroup, Monoid) | ||
deriving stock (Functor, Foldable, Traversable) | ||
#else | ||
newtype RangeMap a = RangeMap | ||
{ unRangeMap :: [(Range, a)] } | ||
deriving newtype (NFData, Semigroup, Monoid) | ||
deriving stock (Functor, Foldable, Traversable) | ||
#endif | ||
|
||
-- | Construct a 'RangeMap' from a 'Range' accessor and a list of values. | ||
fromList :: (a -> Range) -> [a] -> RangeMap a | ||
fromList extractRange = fromList' . map (\x -> (extractRange x, x)) | ||
|
||
fromList' :: [(Range, a)] -> RangeMap a | ||
#ifdef USE_FINGERTREE | ||
fromList' = RangeMap . toIntervalMap . map (first rangeToInterval) | ||
where | ||
toIntervalMap :: Ord v => [(IM.Interval v, a)] -> IM.IntervalMap v a | ||
toIntervalMap = foldl' (\m (i, v) -> IM.insert i v m) IM.empty | ||
#else | ||
fromList' = RangeMap | ||
#endif | ||
|
||
-- | Filter a 'RangeMap' by a given 'Range'. | ||
filterByRange :: Range -> RangeMap a -> [a] | ||
#ifdef USE_FINGERTREE | ||
filterByRange range = map snd . IM.dominators (rangeToInterval range) . unRangeMap | ||
#else | ||
filterByRange range = map snd . filter (isSubrangeOf range . fst) . unRangeMap | ||
#endif | ||
|
||
#ifdef USE_FINGERTREE | ||
rangeToInterval :: Range -> IM.Interval Position | ||
rangeToInterval (Range s e) = IM.Interval s e | ||
ozkutuk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
#endif |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,8 @@ import Ide.Plugin.Conversion (AlternateFormat, | |
ExtensionNeeded (NeedsExtension, NoExtension), | ||
alternateFormat) | ||
import Ide.Plugin.Literals | ||
import Ide.Plugin.RangeMap (RangeMap) | ||
import qualified Ide.Plugin.RangeMap as RangeMap | ||
import Ide.PluginUtils (getNormalizedFilePath, | ||
handleMaybeM, pluginResponse) | ||
import Ide.Types | ||
|
@@ -52,7 +54,7 @@ instance NFData CollectLiterals | |
type instance RuleResult CollectLiterals = CollectLiteralsResult | ||
|
||
data CollectLiteralsResult = CLR | ||
{ literals :: [Literal] | ||
{ literals :: RangeMap Literal | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice |
||
, enabledExtensions :: [GhcExtension] | ||
} deriving (Generic) | ||
|
||
|
@@ -73,25 +75,22 @@ collectLiteralsRule recorder = define (cmapWithPrio LogShake recorder) $ \Collec | |
let exts = map GhcExtension . getExtensions <$> pm | ||
-- collect all the literals for a file | ||
lits = collectLiterals . pm_parsed_source <$> pm | ||
pure ([], CLR <$> lits <*> exts) | ||
litMap = RangeMap.fromList (realSrcSpanToRange . getSrcSpan) <$> lits | ||
pure ([], CLR <$> litMap <*> exts) | ||
|
||
codeActionHandler :: PluginMethodHandler IdeState 'TextDocumentCodeAction | ||
codeActionHandler state pId (CodeActionParams _ _ docId currRange _) = pluginResponse $ do | ||
nfp <- getNormalizedFilePath (docId ^. L.uri) | ||
CLR{..} <- requestLiterals pId state nfp | ||
pragma <- getFirstPragma pId state nfp | ||
-- remove any invalid literals (see validTarget comment) | ||
let litsInRange = filter inCurrentRange literals | ||
let litsInRange = RangeMap.filterByRange currRange literals | ||
-- generate alternateFormats and zip with the literal that generated the alternates | ||
literalPairs = map (\lit -> (lit, alternateFormat lit)) litsInRange | ||
-- make a code action for every literal and its' alternates (then flatten the result) | ||
actions = concatMap (\(lit, alts) -> map (mkCodeAction nfp lit enabledExtensions pragma) alts) literalPairs | ||
pure $ List actions | ||
where | ||
inCurrentRange :: Literal -> Bool | ||
inCurrentRange lit = let srcSpan = getSrcSpan lit | ||
in currRange `contains` srcSpan | ||
|
||
mkCodeAction :: NormalizedFilePath -> Literal -> [GhcExtension] -> NextPragmaInfo -> AlternateFormat -> Command |? CodeAction | ||
mkCodeAction nfp lit enabled npi af@(alt, ext) = InR CodeAction { | ||
_title = mkCodeActionTitle lit af enabled | ||
|
@@ -127,13 +126,6 @@ mkCodeActionTitle lit (alt, ext) ghcExts | |
needsExtension :: Extension -> [GhcExtension] -> Bool | ||
needsExtension ext ghcExts = ext `notElem` map unExt ghcExts | ||
|
||
-- from HaddockComments.hs | ||
contains :: Range -> RealSrcSpan -> Bool | ||
contains Range {_start, _end} x = isInsideRealSrcSpan _start x || isInsideRealSrcSpan _end x | ||
|
||
isInsideRealSrcSpan :: Position -> RealSrcSpan -> Bool | ||
p `isInsideRealSrcSpan` r = let (Range sp ep) = realSrcSpanToRange r in sp <= p && p <= ep | ||
|
||
requestLiterals :: MonadIO m => PluginId -> IdeState -> NormalizedFilePath -> ExceptT String m CollectLiteralsResult | ||
requestLiterals (PluginId pId) state = handleMaybeM "Could not Collect Literals" | ||
. liftIO | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,7 +29,7 @@ test :: TestTree | |
test = testGroup "alternateNumberFormat" [ | ||
codeActionHex "TIntDtoH" 3 13 | ||
, codeActionOctal "TIntDtoO" 3 13 | ||
, codeActionBinary "TIntDtoB" 4 12 | ||
, codeActionBinary "TIntDtoB" 4 13 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what happened here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't cause any worries, it's just the column number to run the code action. I think the column number is arbitrarily in the middle of a number. Looks like it just matches the other test cases. |
||
, codeActionNumDecimal "TIntDtoND" 5 13 | ||
, codeActionFracExp "TFracDtoE" 3 13 | ||
, codeActionFloatHex "TFracDtoHF" 4 13 | ||
|
Uh oh!
There was an error while loading. Please reload this page.