Skip to content

Commit

Permalink
Avoid O(n^2) time broadcast' by replacing Writer
Browse files Browse the repository at this point in the history
With the strict state monad we can prepend the items to a list and then
reverse the list afterwards. This brings the time complexity down from
`O(n^2)` (because of the mappend/list concatenation used by Writer) to
`O(n)`.

Resolves #102.
  • Loading branch information
robbert-vdh committed May 9, 2023
1 parent 9d443e1 commit bd83c69
Showing 1 changed file with 10 additions and 5 deletions.
15 changes: 10 additions & 5 deletions server/src/Icepeak/Server/Subscription.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module Icepeak.Server.Subscription
where

import Control.Monad (void)
import Control.Monad.Writer (Writer, tell, execWriter)
import Control.Monad.State.Strict (State, execState, modify')
import Data.Aeson (Value)
import Data.Foldable (for_, traverse_)
import Data.HashMap.Strict (HashMap)
Expand Down Expand Up @@ -83,20 +83,25 @@ broadcast f path value tree = mapM_ (uncurry f) notifications
-- Like broadcast, but return a list of notifications rather than invoking an
-- effect on each of them.
broadcast' :: [Text] -> Value -> SubscriptionTree id state -> [(state, Value)]
broadcast' = \path value tree -> execWriter $ loop path value tree
broadcast' = \path value tree -> reverse $ execState (loop path value tree) []
where
loop :: [Text] -> Value -> SubscriptionTree id state -> Writer [(state, Value)] ()
-- To prevent this from accidentally blowing up to @O(n^2)@ time, this uses
-- the strict state monad to cons the connections to a list, which then needs
-- to be reversed to get the correct order. This way the operation can be done
-- in @O(n)@ list operations where @n@ is the number of subscribers matched
-- along the path.
loop :: [Text] -> Value -> SubscriptionTree id state -> State [(state, Value)] ()
loop path value (SubscriptionTree here inner) = do
case path of
[] -> do
-- When the path is empty, all subscribers that are "here" or at a deeper
-- level should receive a notification.
traverse_ (\v -> tell [(v, value)]) here
traverse_ (\v -> modify' ((v, value) :)) here
let broadcastInner key = loop [] (Store.lookupOrNull [key] value)
void $ HashMap.traverseWithKey broadcastInner inner

key : pathTail -> do
traverse_ (\v -> tell [(v, value)]) here
traverse_ (\v -> modify' ((v, value) :)) here
for_ (HashMap.lookup key inner) $ \subs ->
loop pathTail (Store.lookupOrNull [key] value) subs

Expand Down

0 comments on commit bd83c69

Please sign in to comment.