From 23dc4c0c7456a3791887012f1cd98e6ee7e7e8f6 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Wed, 23 Oct 2024 11:22:14 +0200 Subject: [PATCH 01/16] integration: Add CLI param to run performance tests separately There is a dummy performace test in this commit to show how it runs. --- integration/Setup.hs | 20 +++++++++----- integration/integration.cabal | 1 + .../test/Performance/BigConversation.hs | 8 ++++++ integration/test/Testlib/Options.hs | 27 ++++++++++++++++++- integration/test/Testlib/Run.hs | 4 ++- 5 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 integration/test/Performance/BigConversation.hs diff --git a/integration/Setup.hs b/integration/Setup.hs index 71b172a7e29..44eabbaf4d0 100644 --- a/integration/Setup.hs +++ b/integration/Setup.hs @@ -27,9 +27,9 @@ import System.Directory import System.FilePath import Prelude -collectTests :: FilePath -> [FilePath] -> IO [(String, String, String, String)] -collectTests pkgRoot roots = - concat <$> traverse (findAllTests . (<> "/Test")) roots +collectTests :: FilePath -> FilePath -> [FilePath] -> IO [(String, String, String, String)] +collectTests pkgRoot topModule roots = + concat <$> traverse (findAllTests . ( topModule)) roots where findAllTests :: FilePath -> IO [(String, String, String, String)] findAllTests root = do @@ -47,7 +47,7 @@ collectTests pkgRoot roots = findModuleTests :: FilePath -> FilePath -> IO [(String, String, String, String)] findModuleTests root path = do - let modl = "Test." <> toModule root path + let modl = topModule <> "." <> toModule root path tests <- collectTestsInModule pkgRoot path pure $ map (\(testName, summary, full) -> (modl, testName, summary, full)) tests @@ -182,8 +182,11 @@ testHooks hooks = for_ (Map.lookup cname (componentNameMap l)) $ \compBIs -> do for_ compBIs $ \compBI -> do let dest = autogenComponentModulesDir l compBI "RunAllTests.hs" - tests <- collectTests (dataDir p) roots - let modules = Set.toList (Set.fromList (map (\(m, _, _, _) -> m) tests)) + tests <- collectTests (dataDir p) "Test" roots + perfTests <- collectTests (dataDir p) "Performance" roots + let modules = Set.toList (Set.fromList (map (\(m, _, _, _) -> m) (tests <> perfTests))) + mkYieldTests testList = + unlines (map (\(m, n, s, f) -> " yieldTests " <> unwords [show m, show n, show s, show f, m <> "." <> n]) testList) createDirectoryIfMissing True (takeDirectory dest) writeFile dest @@ -195,7 +198,10 @@ testHooks hooks = unlines (map ("import qualified " <>) modules), "mkAllTests :: IO [Test]", "mkAllTests = execWriterT $ do", - unlines (map (\(m, n, s, f) -> " yieldTests " <> unwords [show m, show n, show s, show f, m <> "." <> n]) tests) + mkYieldTests tests, + "mkAllPerfTests :: IO [Test]", + "mkAllPerfTests = execWriterT $ do", + mkYieldTests perfTests ] ) pure () diff --git a/integration/integration.cabal b/integration/integration.cabal index 61828917dc8..8c96fc146d5 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -102,6 +102,7 @@ library API.Spar MLS.Util Notifications + Performance.BigConversation RunAllTests SetupHelpers Test.AccessUpdate diff --git a/integration/test/Performance/BigConversation.hs b/integration/test/Performance/BigConversation.hs new file mode 100644 index 00000000000..6572705b7fe --- /dev/null +++ b/integration/test/Performance/BigConversation.hs @@ -0,0 +1,8 @@ +module Performance.BigConversation where + +import SetupHelpers +import Testlib.Prelude + +testBigMLSConversation :: App () +testBigMLSConversation = do + void $ createTeam OwnDomain 50 diff --git a/integration/test/Testlib/Options.hs b/integration/test/Testlib/Options.hs index 094c5951543..2f9134387ef 100644 --- a/integration/test/Testlib/Options.hs +++ b/integration/test/Testlib/Options.hs @@ -1,18 +1,35 @@ -module Testlib.Options (getOptions, TestOptions (..)) where +module Testlib.Options where import Data.List.Split (splitOn) import Options.Applicative import System.Environment (lookupEnv) import Prelude +data TestSuite + = IntegrationSuite + | PerformanceSuite + data TestOptions = TestOptions { includeTests :: [String], excludeTests :: [String], + testSuite :: TestSuite, listTests :: Bool, xmlReport :: Maybe FilePath, configFile :: String } +parseSuite :: String -> Either String TestSuite +parseSuite = \case + "integration" -> Right IntegrationSuite + "performance" -> Right PerformanceSuite + "int" -> Right IntegrationSuite + "perf" -> Right PerformanceSuite + x -> Left $ "Invalid test suite: " <> x + +showSuite :: TestSuite -> String +showSuite IntegrationSuite = "integration" +showSuite PerformanceSuite = "performance" + parser :: Parser TestOptions parser = TestOptions @@ -32,6 +49,14 @@ parser = <> help "Exclude tests matching PATTERN (simple substring match). This flag can be provided multiple times. This flag can also be provided via the TEST_EXCLUDE environment variable." ) ) + <*> option + (eitherReader parseSuite) + ( long "suite" + <> metavar "SUITE" + <> value IntegrationSuite + <> showDefaultWith showSuite + <> help "Test suite to run" + ) <*> switch (long "list" <> short 'l' <> help "Only list tests.") <*> optional ( strOption diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 29a501c03da..27541e4b3c2 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -89,7 +89,9 @@ main = do let f = testFilter opts cfg = opts.configFile - allTests <- mkAllTests + allTests <- case opts.testSuite of + IntegrationSuite -> mkAllTests + PerformanceSuite -> mkAllPerfTests let tests = filter (\(qname, _, _, _) -> f qname) . sortOn (\(qname, _, _, _) -> qname) From 191d26989faf80c31eeb122bd4024b98cb49ae2c Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 24 Oct 2024 09:56:05 +0200 Subject: [PATCH 02/16] run-services: Run services with performance testing overrides --- Makefile | 10 +++- integration/test/Testlib/RunServices.hs | 76 ++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index b04d44051d6..59477df0232 100644 --- a/Makefile +++ b/Makefile @@ -125,6 +125,11 @@ ci-safe: ci: @echo -en "\n\n\nplease choose between goals ci-fast and ci-safe.\n\n\n" +.PHONY: perf-test +perf-test: + make c package=all + ./dist/run-services --mode performance -- ./dist/integration --suite performance + # Compile and run services # Usage: make crun `OR` make crun package=galley .PHONY: cr @@ -132,7 +137,10 @@ cr: c db-migrate ./dist/run-services crm: c db-migrate - ./dist/run-services -m + ./dist/run-services --mode manual + +crp: c db-migrate + ./dist/run-services --mode performance # Run integration from new test suite # Usage: make devtest diff --git a/integration/test/Testlib/RunServices.hs b/integration/test/Testlib/RunServices.hs index a88686b2979..799977ec58f 100644 --- a/integration/test/Testlib/RunServices.hs +++ b/integration/test/Testlib/RunServices.hs @@ -31,8 +31,30 @@ findProjectRoot path = do Nothing -> pure Nothing Just p -> findProjectRoot p +data Mode + = IntegrationTesting + | ManualTesting + | PerformanceTesting + deriving (Show) + +parseMode :: String -> Either String Mode +parseMode = \case + "integration" -> Right IntegrationTesting + "int" -> Right IntegrationTesting + "manual" -> Right ManualTesting + "man" -> Right ManualTesting + "performance" -> Right PerformanceTesting + "perf" -> Right PerformanceTesting + x -> Left $ "Invalid mode: " <> x + +showMode :: Mode -> String +showMode = \case + IntegrationTesting -> "integration" + PerformanceTesting -> "performance" + ManualTesting -> "manual" + data Opts = Opts - { withManualTestingOverrides :: Bool, + { mode :: Mode, runSubprocess :: [String] } deriving (Show) @@ -40,10 +62,13 @@ data Opts = Opts optsParser :: Parser Opts optsParser = Opts - <$> switch - ( long "with-manual-testing-overrides" + <$> option + (eitherReader parseMode) + ( long "mode" <> short 'm' - <> help "Run services with settings tuned for manual app usage (not recommended for running integration tests)" + <> value IntegrationTesting + <> showDefaultWith showMode + <> help "Run services with settings tuned for particular type of testing" ) <*> many ( strArgument @@ -77,11 +102,12 @@ main = do $ do _modifyEnv <- traverseConcurrentlyCodensity - ( \r -> - void - $ if opts.withManualTestingOverrides - then startDynamicBackend r manualTestingOverrides - else startDynamicBackend r mempty + ( \r -> do + let overrides = case opts.mode of + IntegrationTesting -> mempty + PerformanceTesting -> performanceTestingOverrides + ManualTesting -> manualTestingOverrides + void $ startDynamicBackend r overrides ) [backendA, backendB] liftIO run @@ -111,3 +137,35 @@ manualTestingOverrides = >=> setField @_ @Int "optSettings.setUserCookieRenewAge" 1209600 >=> removeField "optSettings.setSuspendInactiveUsers" } + +performanceTestingOverrides :: ServiceOverrides +performanceTestingOverrides = + let authSettings = + object + [ "userTokenTimeout" .= (4838400 :: Int), + "sessionTokenTimeout" .= (86400 :: Int), + "accessTokenTimeout" .= (900 :: Int), + "providerTokenTimeout" .= (900 :: Int), + "legalHoldUserTokenTimeout" .= (4838400 :: Int), + "legalHoldAccessTokenTimeout" .= (900 :: Int) + ] + maxTeamSize = 2000 + maxConvSize = 2000 + maxFanoutSize = 2000 + in def + { brigCfg = + -- Ensure that users don't get logged out quickly, as the tests + -- might need them for longer and logging them out automatically + -- just makes the tests do more annoying things. + mergeField "zauth.authSettings" authSettings + >=> setField @_ @Int "optSettings.setUserCookieRenewAge" 1209600 + >=> removeField "optSettings.setSuspendInactiveUsers" + -- Ensure that big teams and conversations can be created. + >=> setField @_ @Int "optSettings.setMaxTeamSize" maxTeamSize + >=> setField @_ @Int "optSettings.setMaxConvSize" maxConvSize, + galleyCfg = + -- Ensure that big teams and conversations can be created. + setField @_ @Int "settings.maxTeamSize" maxTeamSize + >=> setField @_ @Int "settings.maxFanoutSize" maxFanoutSize + >=> setField @_ @Int "settings.maxConvSize" maxConvSize + } From 8f51ec29f82b31ed29c682ed23759dc2510d7f26 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 24 Oct 2024 12:32:20 +0200 Subject: [PATCH 03/16] WIP: Huge tangent for MLSState --- integration/integration.cabal | 2 +- .../test/Performance/BigConversation.hs | 49 +++++++++++++++++-- integration/test/Test/MLS.hs | 3 ++ integration/test/Testlib/Env.hs | 1 + 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/integration/integration.cabal b/integration/integration.cabal index 8c96fc146d5..7d6f97a46a0 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -22,7 +22,7 @@ custom-setup common common-all default-language: GHC2021 ghc-options: - -Wall -Wpartial-fields -fwarn-tabs -Wno-incomplete-uni-patterns + -Wall -Wpartial-fields -fwarn-tabs -Wno-incomplete-uni-patterns -fmax-errors=1 default-extensions: AllowAmbiguousTypes diff --git a/integration/test/Performance/BigConversation.hs b/integration/test/Performance/BigConversation.hs index 6572705b7fe..69a18aa4946 100644 --- a/integration/test/Performance/BigConversation.hs +++ b/integration/test/Performance/BigConversation.hs @@ -1,8 +1,51 @@ module Performance.BigConversation where +import API.BrigCommon +import Criterion +import Criterion.Main.Options +import Criterion.Types +import qualified Data.ByteString.Base64 as B64 +import qualified Data.Text.Encoding as Text +import MLS.Util import SetupHelpers +import qualified System.CryptoBox as Cryptobox import Testlib.Prelude +import UnliftIO (pooledMapConcurrentlyN) +import UnliftIO.Temporary -testBigMLSConversation :: App () -testBigMLSConversation = do - void $ createTeam OwnDomain 50 +testCreateBigMLSConversation :: App () +testCreateBigMLSConversation = do + (owner, _tid, members) <- createTeam OwnDomain 20 + let genPrekeyInBox box i = do + pk <- assertCrytoboxSuccess =<< liftIO (Cryptobox.newPrekey box i) + pkBS <- liftIO $ Cryptobox.copyBytes pk.prekey + pure $ object ["id" .= i, "key" .= Text.decodeUtf8 (B64.encode pkBS)] + genPrekeys = do + withSystemTempDirectory "cryptobox-prekey-gen" $ \cryptoboxDir -> do + box <- assertCrytoboxSuccess =<< liftIO (Cryptobox.open cryptoboxDir) + firstPrekey <- genPrekeyInBox box 0 + lastPrekey <- genPrekeyInBox box maxBound + pure (firstPrekey, lastPrekey) + createClient user = do + (firstPrekey, lastPrekey) <- genPrekeys + let mlsClientOpts = + def + { clientArgs = + def + { prekeys = Just [firstPrekey], + lastPrekey = Just lastPrekey + } + } + createMLSClient mlsClientOpts user + ownerClient <- createClient owner + _memClients <- pooledMapConcurrentlyN 64 createClient members + createConv <- appToIO $ do + (_, _) <- createNewGroup ownerClient + void $ sendAndConsumeCommitBundle =<< createAddCommit ownerClient members + let benchmarkable = toBenchmarkable (\n -> replicateM_ (fromIntegral n) createConv) + liftIO $ benchmarkWith (defaultConfig {resamples = 5}) benchmarkable + +assertCrytoboxSuccess :: (Show a) => Cryptobox.Result a -> App a +assertCrytoboxSuccess = \case + Cryptobox.Success x -> pure x + e -> assertFailure $ "Cryptobox exception: " <> show e diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 856f480e983..64e6fd8dcb3 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -1,4 +1,5 @@ {-# OPTIONS_GHC -Wno-incomplete-uni-patterns -Wno-ambiguous-fields #-} +{-# OPTIONS_GHC -fmax-errors=10 #-} module Test.MLS where @@ -159,6 +160,7 @@ testMixedProtocolNonTeam secondDomain = do bindResponse (putConversationProtocol bob convId "mixed") $ \resp -> do resp.status `shouldMatchInt` 403 +-- TODO: This test could fail because of not keeping track of protocol testMixedProtocolAddUsers :: (HasCallStack) => Domain -> Ciphersuite -> App () testMixedProtocolAddUsers secondDomain suite = do (alice, tid, _) <- createTeam OwnDomain 1 @@ -197,6 +199,7 @@ testMixedProtocolAddUsers secondDomain suite = do (suiteCode, _) <- assertOne $ T.hexadecimal (T.pack suite.code) resp.json %. "cipher_suite" `shouldMatchInt` suiteCode +-- TODO: This test could fail because of not keeping track of protocol testMixedProtocolUserLeaves :: (HasCallStack) => Domain -> App () testMixedProtocolUserLeaves secondDomain = do (alice, tid, _) <- createTeam OwnDomain 1 diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index f981f64dd52..ac5b58554f5 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -22,6 +22,7 @@ import System.Exit import System.FilePath import System.IO import System.IO.Temp +import Testlib.JSON import Testlib.Prekeys import Testlib.ResourcePool import Testlib.Types From 16740cfaa22d8df8fbb2b373ddf33c0475e54a53 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 24 Oct 2024 14:37:45 +0200 Subject: [PATCH 04/16] WIP: More type errors fixed --- integration/test/Test/MLS.hs | 1 - integration/test/Test/MLS/SubConversation.hs | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 64e6fd8dcb3..28f68930a4a 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -1,5 +1,4 @@ {-# OPTIONS_GHC -Wno-incomplete-uni-patterns -Wno-ambiguous-fields #-} -{-# OPTIONS_GHC -fmax-errors=10 #-} module Test.MLS where diff --git a/integration/test/Test/MLS/SubConversation.hs b/integration/test/Test/MLS/SubConversation.hs index 40cf1e66bf0..68f7eda75f6 100644 --- a/integration/test/Test/MLS/SubConversation.hs +++ b/integration/test/Test/MLS/SubConversation.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -fmax-errors=10 #-} + module Test.MLS.SubConversation where import API.Galley From 878cc00e23d202df7176c4320540023b10fc4035 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 24 Oct 2024 15:47:26 +0200 Subject: [PATCH 05/16] integration: Fix all compile issues --- integration/integration.cabal | 2 +- integration/test/Performance/BigConversation.hs | 8 ++++---- integration/test/Test/MLS/SubConversation.hs | 2 -- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/integration/integration.cabal b/integration/integration.cabal index 7d6f97a46a0..8c96fc146d5 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -22,7 +22,7 @@ custom-setup common common-all default-language: GHC2021 ghc-options: - -Wall -Wpartial-fields -fwarn-tabs -Wno-incomplete-uni-patterns -fmax-errors=1 + -Wall -Wpartial-fields -fwarn-tabs -Wno-incomplete-uni-patterns default-extensions: AllowAmbiguousTypes diff --git a/integration/test/Performance/BigConversation.hs b/integration/test/Performance/BigConversation.hs index 69a18aa4946..953f0f9e7d9 100644 --- a/integration/test/Performance/BigConversation.hs +++ b/integration/test/Performance/BigConversation.hs @@ -15,7 +15,7 @@ import UnliftIO.Temporary testCreateBigMLSConversation :: App () testCreateBigMLSConversation = do - (owner, _tid, members) <- createTeam OwnDomain 20 + (owner, _tid, members) <- createTeam OwnDomain 2000 let genPrekeyInBox box i = do pk <- assertCrytoboxSuccess =<< liftIO (Cryptobox.newPrekey box i) pkBS <- liftIO $ Cryptobox.copyBytes pk.prekey @@ -36,12 +36,12 @@ testCreateBigMLSConversation = do lastPrekey = Just lastPrekey } } - createMLSClient mlsClientOpts user + createMLSClient def mlsClientOpts user ownerClient <- createClient owner _memClients <- pooledMapConcurrentlyN 64 createClient members createConv <- appToIO $ do - (_, _) <- createNewGroup ownerClient - void $ sendAndConsumeCommitBundle =<< createAddCommit ownerClient members + (_, convId) <- createNewGroup def ownerClient + void $ sendAndConsumeCommitBundle =<< createAddCommit ownerClient convId members let benchmarkable = toBenchmarkable (\n -> replicateM_ (fromIntegral n) createConv) liftIO $ benchmarkWith (defaultConfig {resamples = 5}) benchmarkable diff --git a/integration/test/Test/MLS/SubConversation.hs b/integration/test/Test/MLS/SubConversation.hs index 68f7eda75f6..40cf1e66bf0 100644 --- a/integration/test/Test/MLS/SubConversation.hs +++ b/integration/test/Test/MLS/SubConversation.hs @@ -1,5 +1,3 @@ -{-# OPTIONS_GHC -fmax-errors=10 #-} - module Test.MLS.SubConversation where import API.Galley From 863825eb721aecee486e3516fe37da8fcc28e2c1 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 24 Oct 2024 18:07:07 +0200 Subject: [PATCH 06/16] Desperately trying to make the tests work --- integration/test/MLS/Util.hs | 10 +++++----- integration/test/Testlib/Run.hs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index e70fa74d259..4779c4d9f89 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -583,11 +583,11 @@ consumingMessages mlsProtocol mp = Codensity $ \k -> do map (,MLSNotificationMessageTag) (toList oldClients) <> map (,MLSNotificationWelcomeTag) (toList newClients) - let newUsers = - Set.delete mp.sender.user $ - Set.difference - (Set.map (.user) newClients) - (Set.map (.user) oldClients) + -- let newUsers = + -- Set.delete mp.sender.user $ + -- Set.difference + -- (Set.map (.user) newClients) + -- (Set.map (.user) oldClients) withWebSockets (map fst clients) $ \wss -> do r <- k () diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 27541e4b3c2..c9aefb662d3 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -116,7 +116,7 @@ runTests tests mXMLOutput cfg = do runCodensity (mkGlobalEnv cfg) $ \genv -> withAsync displayOutput $ \displayThread -> do -- Currently 4 seems to be stable, more seems to create more timeouts. - report <- fmap mconcat $ pooledForConcurrentlyN 4 tests $ \(qname, _, _, action) -> do + report <- fmap mconcat $ pooledForConcurrentlyN 16 tests $ \(qname, _, _, action) -> do (mErr, tm) <- withTime (runTest genv action) case mErr of Left err -> do From a085d4c49ab2eb814410c916ea8a3f7c235a72a2 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 28 Oct 2024 13:39:32 +0100 Subject: [PATCH 07/16] integration: Wait for the right notification when consuming commit bundles --- integration/test/MLS/Util.hs | 10 +++++----- integration/test/Performance/BigConversation.hs | 2 +- integration/test/Testlib/Env.hs | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index 4779c4d9f89..e70fa74d259 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -583,11 +583,11 @@ consumingMessages mlsProtocol mp = Codensity $ \k -> do map (,MLSNotificationMessageTag) (toList oldClients) <> map (,MLSNotificationWelcomeTag) (toList newClients) - -- let newUsers = - -- Set.delete mp.sender.user $ - -- Set.difference - -- (Set.map (.user) newClients) - -- (Set.map (.user) oldClients) + let newUsers = + Set.delete mp.sender.user $ + Set.difference + (Set.map (.user) newClients) + (Set.map (.user) oldClients) withWebSockets (map fst clients) $ \wss -> do r <- k () diff --git a/integration/test/Performance/BigConversation.hs b/integration/test/Performance/BigConversation.hs index 953f0f9e7d9..0c0f9233d6a 100644 --- a/integration/test/Performance/BigConversation.hs +++ b/integration/test/Performance/BigConversation.hs @@ -40,7 +40,7 @@ testCreateBigMLSConversation = do ownerClient <- createClient owner _memClients <- pooledMapConcurrentlyN 64 createClient members createConv <- appToIO $ do - (_, convId) <- createNewGroup def ownerClient + convId <- createNewGroup def ownerClient void $ sendAndConsumeCommitBundle =<< createAddCommit ownerClient convId members let benchmarkable = toBenchmarkable (\n -> replicateM_ (fromIntegral n) createConv) liftIO $ benchmarkWith (defaultConfig {resamples = 5}) benchmarkable diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index ac5b58554f5..f981f64dd52 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -22,7 +22,6 @@ import System.Exit import System.FilePath import System.IO import System.IO.Temp -import Testlib.JSON import Testlib.Prekeys import Testlib.ResourcePool import Testlib.Types From a28664389fcd0e9a19c1970307ac2ff3fb8c5d38 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 4 Nov 2024 16:10:00 +0100 Subject: [PATCH 08/16] integartion: MLS: Wait for new member notif This breaks mixed protocol tests, so instead of relying on state, make them use a special function. Waiting for the new member notif somehow helps in synchronizing state of conversation across backends before the member can be deleted from the conv. --- integration/test/Test/MLS.hs | 2 -- 1 file changed, 2 deletions(-) diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 28f68930a4a..856f480e983 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -159,7 +159,6 @@ testMixedProtocolNonTeam secondDomain = do bindResponse (putConversationProtocol bob convId "mixed") $ \resp -> do resp.status `shouldMatchInt` 403 --- TODO: This test could fail because of not keeping track of protocol testMixedProtocolAddUsers :: (HasCallStack) => Domain -> Ciphersuite -> App () testMixedProtocolAddUsers secondDomain suite = do (alice, tid, _) <- createTeam OwnDomain 1 @@ -198,7 +197,6 @@ testMixedProtocolAddUsers secondDomain suite = do (suiteCode, _) <- assertOne $ T.hexadecimal (T.pack suite.code) resp.json %. "cipher_suite" `shouldMatchInt` suiteCode --- TODO: This test could fail because of not keeping track of protocol testMixedProtocolUserLeaves :: (HasCallStack) => Domain -> App () testMixedProtocolUserLeaves secondDomain = do (alice, tid, _) <- createTeam OwnDomain 1 From dc85680758b50f98921b5b14cdf602cba8ac1267 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 8 Nov 2024 12:49:48 +0000 Subject: [PATCH 09/16] test concurrency=4 --- integration/test/Testlib/Run.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index c9aefb662d3..27541e4b3c2 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -116,7 +116,7 @@ runTests tests mXMLOutput cfg = do runCodensity (mkGlobalEnv cfg) $ \genv -> withAsync displayOutput $ \displayThread -> do -- Currently 4 seems to be stable, more seems to create more timeouts. - report <- fmap mconcat $ pooledForConcurrentlyN 16 tests $ \(qname, _, _, action) -> do + report <- fmap mconcat $ pooledForConcurrentlyN 4 tests $ \(qname, _, _, action) -> do (mErr, tm) <- withTime (runTest genv action) case mErr of Left err -> do From 84857029a7a7b51f54e8916c0208ae31c838a8eb Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 8 Nov 2024 13:51:38 +0000 Subject: [PATCH 10/16] test suite env var --- integration/test/Testlib/Options.hs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/integration/test/Testlib/Options.hs b/integration/test/Testlib/Options.hs index 2f9134387ef..2c887e7512b 100644 --- a/integration/test/Testlib/Options.hs +++ b/integration/test/Testlib/Options.hs @@ -1,6 +1,8 @@ module Testlib.Options where +import Data.Either.Extra (eitherToMaybe) import Data.List.Split (splitOn) +import Data.Maybe (fromMaybe) import Options.Applicative import System.Environment (lookupEnv) import Prelude @@ -55,7 +57,7 @@ parser = <> metavar "SUITE" <> value IntegrationSuite <> showDefaultWith showSuite - <> help "Test suite to run" + <> help "Test suite to run. This flag can also be provided via the TEST_SUITE environment variable." ) <*> switch (long "list" <> short 'l' <> help "Only list tests.") <*> optional @@ -86,13 +88,15 @@ getOptions :: IO TestOptions getOptions = do defaultsInclude <- maybe [] (splitOn ",") <$> lookupEnv "TEST_INCLUDE" defaultsExclude <- maybe [] (splitOn ",") <$> lookupEnv "TEST_EXCLUDE" + defaultsSuite <- (>>= eitherToMaybe . parseSuite) <$> lookupEnv "TEST_SUITE" defaultsXMLReport <- lookupEnv "TEST_XML" opts <- execParser optInfo pure opts { includeTests = includeTests opts `orFromEnv` defaultsInclude, excludeTests = excludeTests opts `orFromEnv` defaultsExclude, - xmlReport = xmlReport opts `orFromEnv` defaultsXMLReport + xmlReport = xmlReport opts `orFromEnv` defaultsXMLReport, + testSuite = fromMaybe opts.testSuite defaultsSuite } where orFromEnv fromArgs fromEnv = From 570aad274a9f9092fd23d14976bc100f60bd5464 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 15 Nov 2024 09:14:33 +0000 Subject: [PATCH 11/16] html output --- .../test/Performance/BigConversation.hs | 27 ++++++++++++++----- integration/test/Testlib/RunServices.hs | 6 ++--- .../src/Wire/API/Team/HardTruncationLimit.hs | 3 ++- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/integration/test/Performance/BigConversation.hs b/integration/test/Performance/BigConversation.hs index 0c0f9233d6a..33ffc89fbfc 100644 --- a/integration/test/Performance/BigConversation.hs +++ b/integration/test/Performance/BigConversation.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wwarn #-} + module Performance.BigConversation where import API.BrigCommon @@ -12,10 +14,15 @@ import qualified System.CryptoBox as Cryptobox import Testlib.Prelude import UnliftIO (pooledMapConcurrentlyN) import UnliftIO.Temporary +import Criterion.Main (runMode) +import Options.Applicative.Common (evalParser) +import Control.Monad.Reader (MonadReader(ask)) testCreateBigMLSConversation :: App () testCreateBigMLSConversation = do - (owner, _tid, members) <- createTeam OwnDomain 2000 + let teamSize = 20 + let clientsPerUser = 1 + (owner, _tid, members) <- createTeam OwnDomain teamSize let genPrekeyInBox box i = do pk <- assertCrytoboxSuccess =<< liftIO (Cryptobox.newPrekey box i) pkBS <- liftIO $ Cryptobox.copyBytes pk.prekey @@ -38,12 +45,18 @@ testCreateBigMLSConversation = do } createMLSClient def mlsClientOpts user ownerClient <- createClient owner - _memClients <- pooledMapConcurrentlyN 64 createClient members - createConv <- appToIO $ do - convId <- createNewGroup def ownerClient - void $ sendAndConsumeCommitBundle =<< createAddCommit ownerClient convId members - let benchmarkable = toBenchmarkable (\n -> replicateM_ (fromIntegral n) createConv) - liftIO $ benchmarkWith (defaultConfig {resamples = 5}) benchmarkable + _memClients <- pooledMapConcurrentlyN 64 (replicateM_ clientsPerUser . createClient) members + let createConv n = do + convId <- createNewGroup def ownerClient + void $ sendAndConsumeCommitBundle =<< createAddCommit ownerClient convId (take n members) + let conf = defaultConfig { reportFile = Just $ "big-conversation-" <> show clientsPerUser <> "-clients-per-user.html" } + case evalParser $ parseWith conf of + Nothing -> assertFailure "Failed to parse criterion options" + Just mode -> do + e <- ask + let mkBenchmark n = bench ("conversation with " <> show n <> " members") $ nfIO (runAppWithEnv e $ createConv n) + let benchmarks = mkBenchmark <$> [10] + liftIO $ runMode mode benchmarks assertCrytoboxSuccess :: (Show a) => Cryptobox.Result a -> App a assertCrytoboxSuccess = \case diff --git a/integration/test/Testlib/RunServices.hs b/integration/test/Testlib/RunServices.hs index 799977ec58f..3be1711bae2 100644 --- a/integration/test/Testlib/RunServices.hs +++ b/integration/test/Testlib/RunServices.hs @@ -149,9 +149,9 @@ performanceTestingOverrides = "legalHoldUserTokenTimeout" .= (4838400 :: Int), "legalHoldAccessTokenTimeout" .= (900 :: Int) ] - maxTeamSize = 2000 - maxConvSize = 2000 - maxFanoutSize = 2000 + maxTeamSize = 6000 + maxConvSize = 6000 + maxFanoutSize = 6000 in def { brigCfg = -- Ensure that users don't get logged out quickly, as the tests diff --git a/libs/wire-api/src/Wire/API/Team/HardTruncationLimit.hs b/libs/wire-api/src/Wire/API/Team/HardTruncationLimit.hs index 47845ffc6a7..b7f2d907d8c 100644 --- a/libs/wire-api/src/Wire/API/Team/HardTruncationLimit.hs +++ b/libs/wire-api/src/Wire/API/Team/HardTruncationLimit.hs @@ -4,7 +4,8 @@ import Data.Proxy import GHC.TypeLits import Imports -type HardTruncationLimit = (2000 :: Nat) +-- TODO: do not merge this change... +type HardTruncationLimit = (6000 :: Nat) hardTruncationLimit :: (Integral a) => a hardTruncationLimit = fromIntegral $ natVal (Proxy @HardTruncationLimit) From 829ec226413f300796947bcd311224beefc003e7 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 19 Nov 2024 13:15:50 +0000 Subject: [PATCH 12/16] create conv in chunks --- integration/test/MLS/Util.hs | 2 + .../test/Performance/BigConversation.hs | 50 +++++++++++-------- integration/test/Testlib/RunServices.hs | 6 +-- .../src/Wire/API/Team/HardTruncationLimit.hs | 2 +- 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index e70fa74d259..897ebaec00f 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -326,6 +326,7 @@ createAddCommit cid convId users = do kps <- fmap concat . for users $ \user -> do bundle <- claimKeyPackages conv.ciphersuite cid user >>= getJSON 200 unbundleKeyPackages bundle + putStrLn $ "Creating commit with " <> show (length kps) <> " clients" createAddCommitWithKeyPackages cid convId kps withTempKeyPackageFile :: ByteString -> ContT a App FilePath @@ -371,6 +372,7 @@ createAddCommitWithKeyPackages cid convId clientsAndKeyPackages = do ) Nothing + liftIO $ appendFile "commit-sizes.txt" ((show (BS.length commit)) <> "\n") modifyMLSState $ \mls -> mls { convs = diff --git a/integration/test/Performance/BigConversation.hs b/integration/test/Performance/BigConversation.hs index 33ffc89fbfc..288af62ac61 100644 --- a/integration/test/Performance/BigConversation.hs +++ b/integration/test/Performance/BigConversation.hs @@ -3,26 +3,43 @@ module Performance.BigConversation where import API.BrigCommon -import Criterion -import Criterion.Main.Options -import Criterion.Types +import qualified Data.ByteString as BS import qualified Data.ByteString.Base64 as B64 +import Data.List.Extra (chunksOf) import qualified Data.Text.Encoding as Text +import Data.Time (NominalDiffTime, diffUTCTime, getCurrentTime) import MLS.Util import SetupHelpers import qualified System.CryptoBox as Cryptobox import Testlib.Prelude import UnliftIO (pooledMapConcurrentlyN) import UnliftIO.Temporary -import Criterion.Main (runMode) -import Options.Applicative.Common (evalParser) -import Control.Monad.Reader (MonadReader(ask)) testCreateBigMLSConversation :: App () testCreateBigMLSConversation = do - let teamSize = 20 - let clientsPerUser = 1 - (owner, _tid, members) <- createTeam OwnDomain teamSize + let teamSize = 100 + let batchSize = 10 + (_, ownerClient, _, members, _) <- createTeamAndClients teamSize + convId <- createNewGroup def ownerClient + let memberChunks = chunksOf batchSize members + for_ memberChunks $ \chunk -> do + (size, time) <- timeIt $ do + msg <- createAddCommit ownerClient convId chunk + void $ sendAndConsumeCommitBundle msg + pure (BS.length msg.message) + putStrLn $ "Sent " <> show size <> " bytes in " <> show time + pure (size, time) + +timeIt :: App a -> App (a, NominalDiffTime) +timeIt action = do + start <- liftIO getCurrentTime + result <- action + end <- liftIO getCurrentTime + pure (result, diffUTCTime end start) + +createTeamAndClients :: Int -> App (Value, ClientIdentity, String, [Value], [ClientIdentity]) +createTeamAndClients teamSize = do + (owner, tid, members) <- createTeam OwnDomain teamSize let genPrekeyInBox box i = do pk <- assertCrytoboxSuccess =<< liftIO (Cryptobox.newPrekey box i) pkBS <- liftIO $ Cryptobox.copyBytes pk.prekey @@ -45,18 +62,9 @@ testCreateBigMLSConversation = do } createMLSClient def mlsClientOpts user ownerClient <- createClient owner - _memClients <- pooledMapConcurrentlyN 64 (replicateM_ clientsPerUser . createClient) members - let createConv n = do - convId <- createNewGroup def ownerClient - void $ sendAndConsumeCommitBundle =<< createAddCommit ownerClient convId (take n members) - let conf = defaultConfig { reportFile = Just $ "big-conversation-" <> show clientsPerUser <> "-clients-per-user.html" } - case evalParser $ parseWith conf of - Nothing -> assertFailure "Failed to parse criterion options" - Just mode -> do - e <- ask - let mkBenchmark n = bench ("conversation with " <> show n <> " members") $ nfIO (runAppWithEnv e $ createConv n) - let benchmarks = mkBenchmark <$> [10] - liftIO $ runMode mode benchmarks + memClients <- pooledMapConcurrentlyN 64 createClient members + for_ memClients $ uploadNewKeyPackage def + pure (owner, ownerClient, tid, members, memClients) assertCrytoboxSuccess :: (Show a) => Cryptobox.Result a -> App a assertCrytoboxSuccess = \case diff --git a/integration/test/Testlib/RunServices.hs b/integration/test/Testlib/RunServices.hs index 3be1711bae2..5b79c434068 100644 --- a/integration/test/Testlib/RunServices.hs +++ b/integration/test/Testlib/RunServices.hs @@ -149,9 +149,9 @@ performanceTestingOverrides = "legalHoldUserTokenTimeout" .= (4838400 :: Int), "legalHoldAccessTokenTimeout" .= (900 :: Int) ] - maxTeamSize = 6000 - maxConvSize = 6000 - maxFanoutSize = 6000 + maxTeamSize = 65000 + maxConvSize = 65000 + maxFanoutSize = 65000 in def { brigCfg = -- Ensure that users don't get logged out quickly, as the tests diff --git a/libs/wire-api/src/Wire/API/Team/HardTruncationLimit.hs b/libs/wire-api/src/Wire/API/Team/HardTruncationLimit.hs index b7f2d907d8c..c8fe1ee4b68 100644 --- a/libs/wire-api/src/Wire/API/Team/HardTruncationLimit.hs +++ b/libs/wire-api/src/Wire/API/Team/HardTruncationLimit.hs @@ -5,7 +5,7 @@ import GHC.TypeLits import Imports -- TODO: do not merge this change... -type HardTruncationLimit = (6000 :: Nat) +type HardTruncationLimit = (65000 :: Nat) hardTruncationLimit :: (Integral a) => a hardTruncationLimit = fromIntegral $ natVal (Proxy @HardTruncationLimit) From a1df7ddced30552ae37e7155a454532248688fde Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 19 Nov 2024 13:51:13 +0000 Subject: [PATCH 13/16] remove logging --- integration/test/MLS/Util.hs | 2 -- 1 file changed, 2 deletions(-) diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index 897ebaec00f..e70fa74d259 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -326,7 +326,6 @@ createAddCommit cid convId users = do kps <- fmap concat . for users $ \user -> do bundle <- claimKeyPackages conv.ciphersuite cid user >>= getJSON 200 unbundleKeyPackages bundle - putStrLn $ "Creating commit with " <> show (length kps) <> " clients" createAddCommitWithKeyPackages cid convId kps withTempKeyPackageFile :: ByteString -> ContT a App FilePath @@ -372,7 +371,6 @@ createAddCommitWithKeyPackages cid convId clientsAndKeyPackages = do ) Nothing - liftIO $ appendFile "commit-sizes.txt" ((show (BS.length commit)) <> "\n") modifyMLSState $ \mls -> mls { convs = From 3b82f4d02666ed3fff15bab9ce6f56c3aa6bb01b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 19 Nov 2024 15:32:07 +0100 Subject: [PATCH 14/16] Create the ConversationSize test type --- .../test/Performance/BigConversation.hs | 58 ++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/integration/test/Performance/BigConversation.hs b/integration/test/Performance/BigConversation.hs index 288af62ac61..8c67daf6242 100644 --- a/integration/test/Performance/BigConversation.hs +++ b/integration/test/Performance/BigConversation.hs @@ -15,20 +15,50 @@ import Testlib.Prelude import UnliftIO (pooledMapConcurrentlyN) import UnliftIO.Temporary -testCreateBigMLSConversation :: App () -testCreateBigMLSConversation = do - let teamSize = 100 - let batchSize = 10 - (_, ownerClient, _, members, _) <- createTeamAndClients teamSize - convId <- createNewGroup def ownerClient - let memberChunks = chunksOf batchSize members - for_ memberChunks $ \chunk -> do - (size, time) <- timeIt $ do - msg <- createAddCommit ownerClient convId chunk - void $ sendAndConsumeCommitBundle msg - pure (BS.length msg.message) - putStrLn $ "Sent " <> show size <> " bytes in " <> show time - pure (size, time) +-- | A size saying how big an MLS conversation is. Each size is mapped to a +-- number via the 'sizeToNumber' function. +data ConversationSize + = Tiny + | Small + | Medium + | Big + | Large + | VeryLarge + deriving (Eq, Generic, Show) + +sizeToNumber :: ConversationSize -> Word +sizeToNumber Tiny = 20 +sizeToNumber Small = 100 +sizeToNumber Medium = 500 +sizeToNumber Big = 1000 +sizeToNumber Large = 5000 +sizeToNumber VeryLarge = 10000 + +batchForSize :: ConversationSize -> Word +batchForSize Tiny = 10 +batchForSize Small = 20 +batchForSize Medium = 100 +batchForSize Big = 100 +batchForSize Large = 250 +batchForSize VeryLarge = 500 + +testCreateBigMLSConversation :: ConversationSize -> App () +testCreateBigMLSConversation convSize = do + let teamSize = sizeToNumber convSize + let batchSize = fromIntegral . batchForSize $ convSize + totalTime <- + fmap snd $ timeIt do + (_, ownerClient, _, members, _) <- createTeamAndClients . fromIntegral $ teamSize + convId <- createNewGroup def ownerClient + let memberChunks = chunksOf batchSize members + for_ memberChunks $ \chunk -> do + (size, time) <- timeIt $ do + msg <- createAddCommit ownerClient convId chunk + void $ sendAndConsumeCommitBundle msg + pure (BS.length msg.message) + putStrLn $ "Sent " <> show size <> " bytes in " <> show time + pure (size, time) + putStrLn $ "Total time: " <> show totalTime timeIt :: App a -> App (a, NominalDiffTime) timeIt action = do From 838266d30ea1336cc6dba36a13c841f5344dc096 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 20 Nov 2024 10:19:16 +0000 Subject: [PATCH 15/16] use new client capability of consumable notifications --- integration/integration.cabal | 1 + integration/test/MLS/Util.hs | 114 +++++++++ .../test/Performance/BigConversation.hs | 66 +++-- integration/test/Test/Events.hs | 138 +---------- .../Testlib/Cannon/ConsumableNotifications.hs | 226 ++++++++++++++++++ 5 files changed, 394 insertions(+), 151 deletions(-) create mode 100644 integration/test/Testlib/Cannon/ConsumableNotifications.hs diff --git a/integration/integration.cabal b/integration/integration.cabal index 8c96fc146d5..3675df478f6 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -179,6 +179,7 @@ library Testlib.App Testlib.Assertions Testlib.Cannon + Testlib.Cannon.ConsumableNotifications Testlib.Certs Testlib.Env Testlib.HTTP diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index e70fa74d259..ed793acfb75 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -38,6 +38,7 @@ import System.IO.Temp import System.Posix.Files import System.Process import Testlib.Assertions +import qualified Testlib.Cannon.ConsumableNotifications as CN import Testlib.HTTP import Testlib.JSON import Testlib.Prelude @@ -570,6 +571,78 @@ createExternalCommit convId cid mgi = do data MLSNotificationTag = MLSNotificationMessageTag | MLSNotificationWelcomeTag deriving (Show, Eq, Ord) +consumingMassagesViaNewCapability :: (HasCallStack) => MLSProtocol -> MessagePackage -> Codensity App () +consumingMassagesViaNewCapability mlsProtocol mp = Codensity $ \k -> do + conv <- getMLSConv mp.convId + -- clients that should receive the message itself + let oldClients = Set.delete mp.sender conv.members + -- clients that should receive a welcome message + let newClients = Set.delete mp.sender conv.newMembers + -- all clients that should receive some MLS notification, together with the + -- expected notification tag + let clients = + map (,MLSNotificationMessageTag) (toList oldClients) + <> map (,MLSNotificationWelcomeTag) (toList newClients) + + let newUsers = + Set.delete mp.sender.user $ + Set.difference + (Set.map (.user) newClients) + (Set.map (.user) oldClients) + + let uidsWithClients = + fmap + ((\c -> (c.user, (object ["domain" .= c.domain, "id" .= c.user], c.client))) . fst) + clients + + CN.withEventsWebSockets (fmap snd uidsWithClients) $ \chans -> do + r <- k () + + -- if the conversation is actually MLS (and not mixed), pick one client for + -- each new user and wait for its join event. In Mixed protocol, the user is + -- already in the conversation so they do not get a member-join + -- notification. + when (mlsProtocol == MLSProtocolMLS) $ do + let uidsWithChannels = zip uidsWithClients chans + let newUserChans = uidsWithChannels & filter (\((uid, _), _) -> Set.member uid newUsers) & fmap snd + let assertJoin e = do + eventType <- e %. "data.event.payload.0.type" & asString + pure $ eventType == "conversation.member-join" + + traverse_ + ( \(eventChan, ackChan) -> + CN.awaitMatch assertJoin eventChan + >>= CN.ackEvent ackChan + ) + newUserChans + + -- at this point we know that every new user has been added to the + -- conversation + for_ (zip clients chans) $ \((cid, t), (eventChan, ackChan)) -> case t of + MLSNotificationMessageTag -> do + event <- + CN.awaitMatch + ( \e -> do + eventType <- e %. "data.event.payload.0.type" & asString + pure $ eventType == "conversation.mls-message-add" + ) + eventChan + CN.ackEvent ackChan event + eventData <- event %. "data.event.payload.0.data" & asByteString + void $ mlsCliConsume mp.convId conv.ciphersuite cid eventData + MLSNotificationWelcomeTag -> do + event <- + CN.awaitMatch + ( \e -> do + eventType <- e %. "data.event.payload.0.type" & asString + pure $ eventType == "conversation.mls-welcome" + ) + eventChan + CN.ackEvent ackChan event + eventData <- event %. "data.event.payload.0.data" & asByteString + void $ fromWelcome mp.convId conv.ciphersuite cid eventData + pure r + consumingMessages :: (HasCallStack) => MLSProtocol -> MessagePackage -> Codensity App () consumingMessages mlsProtocol mp = Codensity $ \k -> do conv <- getMLSConv mp.convId @@ -588,6 +661,7 @@ consumingMessages mlsProtocol mp = Codensity $ \k -> do Set.difference (Set.map (.user) newClients) (Set.map (.user) oldClients) + withWebSockets (map fst clients) $ \wss -> do r <- k () @@ -863,3 +937,43 @@ getSubConvId user convId subConvName = getSubConversation user convId subConvName >>= getJSON 200 >>= objConvId + +-- FUTUREWORK: we assume all clients in the conversation have the new consumable-notification capability +-- to support both the legacy and the new capability, +-- we need to add it to the client identity and store it in the local MLS state +sendAndConsumeCommitBundleNew :: (HasCallStack) => MessagePackage -> App Value +sendAndConsumeCommitBundleNew = sendAndConsumeCommitBundleWithProtocolNew MLSProtocolMLS + +-- | Send an MLS commit bundle, wait for clients to receive it, consume it, and +-- update the test state accordingly. +sendAndConsumeCommitBundleWithProtocolNew :: (HasCallStack) => MLSProtocol -> MessagePackage -> App Value +sendAndConsumeCommitBundleWithProtocolNew protocol mp = do + lowerCodensity $ do + consumingMassagesViaNewCapability protocol mp + lift $ do + r <- postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 201 + + -- if the sender is a new member (i.e. it's an external commit), then + -- process the welcome message directly + do + conv <- getMLSConv mp.convId + when (Set.member mp.sender conv.newMembers) $ + traverse_ (fromWelcome mp.convId conv.ciphersuite mp.sender) mp.welcome + + -- increment epoch and add new clients + modifyMLSState $ \mls -> + mls + { convs = + Map.adjust + ( \conv -> + conv + { epoch = conv.epoch + 1, + members = conv.members <> conv.newMembers, + newMembers = mempty + } + ) + mp.convId + mls.convs + } + + pure r diff --git a/integration/test/Performance/BigConversation.hs b/integration/test/Performance/BigConversation.hs index 8c67daf6242..4bd2b798125 100644 --- a/integration/test/Performance/BigConversation.hs +++ b/integration/test/Performance/BigConversation.hs @@ -3,6 +3,7 @@ module Performance.BigConversation where import API.BrigCommon +import API.Galley (getConversation) import qualified Data.ByteString as BS import qualified Data.ByteString.Base64 as B64 import Data.List.Extra (chunksOf) @@ -12,8 +13,9 @@ import MLS.Util import SetupHelpers import qualified System.CryptoBox as Cryptobox import Testlib.Prelude -import UnliftIO (pooledMapConcurrentlyN) +import UnliftIO (modifyIORef', newIORef, pooledMapConcurrentlyN, readIORef) import UnliftIO.Temporary +import Prelude (writeFile) -- | A size saying how big an MLS conversation is. Each size is mapped to a -- number via the 'sizeToNumber' function. @@ -38,27 +40,54 @@ batchForSize :: ConversationSize -> Word batchForSize Tiny = 10 batchForSize Small = 20 batchForSize Medium = 100 -batchForSize Big = 100 -batchForSize Large = 250 +batchForSize Big = 250 +batchForSize Large = 500 batchForSize VeryLarge = 500 -testCreateBigMLSConversation :: ConversationSize -> App () -testCreateBigMLSConversation convSize = do - let teamSize = sizeToNumber convSize - let batchSize = fromIntegral . batchForSize $ convSize - totalTime <- - fmap snd $ timeIt do - (_, ownerClient, _, members, _) <- createTeamAndClients . fromIntegral $ teamSize +testCreateBigMLSConversation :: App () +testCreateBigMLSConversation = do + domain <- OwnDomain & asString + let teamSize = 11 + let batchSize = 20 + let clientNotifCapability = Consumable + putStrLn $ "Creating a team with " <> show teamSize <> " members" + (owner, ownerClient, _, members, c1 : c2 : _) <- createTeamAndClients domain clientNotifCapability teamSize + putStrLn $ "Creating a conversation with " <> show teamSize <> " members in batches of " <> show batchSize + convSize <- liftIO $ newIORef (1 :: Int) + (convId, totalTime) <- + timeIt do convId <- createNewGroup def ownerClient let memberChunks = chunksOf batchSize members for_ memberChunks $ \chunk -> do (size, time) <- timeIt $ do msg <- createAddCommit ownerClient convId chunk - void $ sendAndConsumeCommitBundle msg + void $ case clientNotifCapability of + Legacy -> sendAndConsumeCommitBundle msg + Consumable -> sendAndConsumeCommitBundleNew msg pure (BS.length msg.message) - putStrLn $ "Sent " <> show size <> " bytes in " <> show time + cs <- liftIO $ readIORef convSize + putStrLn $ "Sent " <> show size <> " bytes in " <> show time <> ", adding " <> show (length chunk) <> " members to conv of size: " <> show cs + liftIO $ modifyIORef' convSize (+ (length chunk)) pure (size, time) + pure convId putStrLn $ "Total time: " <> show totalTime + do + conv <- getConversation owner (convIdToQidObject convId) >>= getJSON 200 + otherMembers <- conv %. "members.others" & asList + length otherMembers `shouldMatchInt` (teamSize - 1) + (bytes, timeRemoval) <- timeIt $ do + commit <- createRemoveCommit ownerClient convId [c1, c2] + -- m <- showMessage def ownerClient commit.message + -- prettyJSON m >>= liftIO . writeFile "removal.json" + case clientNotifCapability of + Legacy -> void $ sendAndConsumeCommitBundle commit + Consumable -> void $ sendAndConsumeCommitBundleNew commit + pure (BS.length commit.message) + putStrLn $ "Sent " <> show bytes <> " bytes in " <> show timeRemoval <> " for removing 2 members" + do + conv <- getConversation owner (convIdToQidObject convId) >>= getJSON 200 + otherMembers <- conv %. "members.others" & asList + length otherMembers `shouldMatchInt` (teamSize - 3) timeIt :: App a -> App (a, NominalDiffTime) timeIt action = do @@ -67,9 +96,9 @@ timeIt action = do end <- liftIO getCurrentTime pure (result, diffUTCTime end start) -createTeamAndClients :: Int -> App (Value, ClientIdentity, String, [Value], [ClientIdentity]) -createTeamAndClients teamSize = do - (owner, tid, members) <- createTeam OwnDomain teamSize +createTeamAndClients :: String -> ClientNotifCapability -> Int -> App (Value, ClientIdentity, String, [Value], [ClientIdentity]) +createTeamAndClients domain clientNotifCapability teamSize = do + (owner, tid, members) <- createTeam domain teamSize let genPrekeyInBox box i = do pk <- assertCrytoboxSuccess =<< liftIO (Cryptobox.newPrekey box i) pkBS <- liftIO $ Cryptobox.copyBytes pk.prekey @@ -87,7 +116,10 @@ createTeamAndClients teamSize = do { clientArgs = def { prekeys = Just [firstPrekey], - lastPrekey = Just lastPrekey + lastPrekey = Just lastPrekey, + acapabilities = case clientNotifCapability of + Legacy -> Nothing + Consumable -> Just ["consumable-notifications"] } } createMLSClient def mlsClientOpts user @@ -100,3 +132,5 @@ assertCrytoboxSuccess :: (Show a) => Cryptobox.Result a -> App a assertCrytoboxSuccess = \case Cryptobox.Success x -> pure x e -> assertFailure $ "Cryptobox exception: " <> show e + +data ClientNotifCapability = Legacy | Consumable diff --git a/integration/test/Test/Events.hs b/integration/test/Test/Events.hs index 29f6fd4f945..ed4a678c5aa 100644 --- a/integration/test/Test/Events.hs +++ b/integration/test/Test/Events.hs @@ -7,14 +7,12 @@ import API.Galley import API.Gundeck import qualified Control.Concurrent.Timeout as Timeout import Control.Retry -import Data.ByteString.Conversion (toByteString') -import qualified Data.Text as Text import Data.Timeout -import qualified Network.WebSockets as WS import Notifications import SetupHelpers +import Testlib.Cannon.ConsumableNotifications import Testlib.Prelude hiding (assertNoEvent) -import Testlib.Printing +import qualified Testlib.Prelude as Old import UnliftIO hiding (handle) -- FUTUREWORK: Investigate why these tests are failing without @@ -105,7 +103,7 @@ testConsumeEventsWhileHavingLegacyClients = do newClient <- addClient alice def {acapabilities = Just ["consumable-notifications"]} >>= getJSON 201 newClientId <- newClient %. "id" & asString - oldNotif <- awaitMatch isUserClientAddNotif oldWS + oldNotif <- Old.awaitMatch isUserClientAddNotif oldWS oldNotif %. "payload.0.client.id" `shouldMatch` newClientId withEventsWebSocket alice newClientId $ \eventsChan _ -> @@ -303,133 +301,3 @@ testTransientEvents = do ackEvent ackChan e assertNoEvent eventsChan - ----------------------------------------------------------------------- --- helpers - -withEventsWebSockets :: forall uid a. (HasCallStack, MakesValue uid) => [(uid, String)] -> ([(TChan Value, TChan Value)] -> App a) -> App a -withEventsWebSockets userClients k = go [] $ reverse userClients - where - go :: [(TChan Value, TChan Value)] -> [(uid, String)] -> App a - go chans [] = k chans - go chans ((uid, cid) : remaining) = - withEventsWebSocket uid cid $ \eventsChan ackChan -> - go ((eventsChan, ackChan) : chans) remaining - -withEventsWebSocket :: (HasCallStack, MakesValue uid) => uid -> String -> (TChan Value -> TChan Value -> App a) -> App a -withEventsWebSocket uid cid k = do - closeWS <- newEmptyMVar - bracket (setup closeWS) (\(_, _, wsThread) -> cancel wsThread) $ \(eventsChan, ackChan, wsThread) -> do - x <- k eventsChan ackChan - - -- Ensure all the acks are sent before closing the websocket - isAckChanEmpty <- - retrying - (limitRetries 5 <> constantDelay 10_000) - (\_ isEmpty -> pure $ not isEmpty) - (\_ -> atomically $ isEmptyTChan ackChan) - unless isAckChanEmpty $ do - putStrLn $ colored yellow $ "The ack chan is not empty after 50ms, some acks may not make it to the server" - - void $ tryPutMVar closeWS () - - timeout 1_000_000 (wait wsThread) >>= \case - Nothing -> - putStrLn $ colored yellow $ "The websocket thread did not close after waiting for 1s" - Just () -> pure () - - pure x - where - setup :: (HasCallStack) => MVar () -> App (TChan Value, TChan Value, Async ()) - setup closeWS = do - (eventsChan, ackChan) <- liftIO $ (,) <$> newTChanIO <*> newTChanIO - wsThread <- eventsWebSocket uid cid eventsChan ackChan closeWS - pure (eventsChan, ackChan, wsThread) - -sendMsg :: (HasCallStack) => TChan Value -> Value -> App () -sendMsg eventsChan msg = liftIO $ atomically $ writeTChan eventsChan msg - -ackFullSync :: (HasCallStack) => TChan Value -> App () -ackFullSync ackChan = do - sendMsg ackChan - $ object ["type" .= "ack_full_sync"] - -ackEvent :: (HasCallStack) => TChan Value -> Value -> App () -ackEvent ackChan event = do - deliveryTag <- event %. "data.delivery_tag" - sendAck ackChan deliveryTag False - -sendAck :: (HasCallStack) => TChan Value -> Value -> Bool -> App () -sendAck ackChan deliveryTag multiple = do - sendMsg ackChan - $ object - [ "type" .= "ack", - "data" - .= object - [ "delivery_tag" .= deliveryTag, - "multiple" .= multiple - ] - ] - -assertEvent :: (HasCallStack) => TChan Value -> ((HasCallStack) => Value -> App a) -> App a -assertEvent eventsChan expectations = do - timeout 10_000_000 (atomically (readTChan eventsChan)) >>= \case - Nothing -> assertFailure "No event received for 10s" - Just e -> do - pretty <- prettyJSON e - addFailureContext ("event:\n" <> pretty) - $ expectations e - -assertNoEvent :: (HasCallStack) => TChan Value -> App () -assertNoEvent eventsChan = do - timeout 1_000_000 (atomically (readTChan eventsChan)) >>= \case - Nothing -> pure () - Just e -> do - eventJSON <- prettyJSON e - assertFailure $ "Did not expect event: \n" <> eventJSON - -consumeAllEvents :: TChan Value -> TChan Value -> App () -consumeAllEvents eventsChan ackChan = do - timeout 1_000_000 (atomically (readTChan eventsChan)) >>= \case - Nothing -> pure () - Just e -> do - ackEvent ackChan e - consumeAllEvents eventsChan ackChan - -eventsWebSocket :: (MakesValue user) => user -> String -> TChan Value -> TChan Value -> MVar () -> App (Async ()) -eventsWebSocket user clientId eventsChan ackChan closeWS = do - serviceMap <- getServiceMap =<< objDomain user - uid <- objId =<< objQidObject user - let HostPort caHost caPort = serviceHostPort serviceMap Cannon - path = "/events?client=" <> clientId - caHdrs = [(fromString "Z-User", toByteString' uid)] - app conn = do - r <- - async $ wsRead conn `catch` \(e :: WS.ConnectionException) -> - case e of - WS.CloseRequest {} -> pure () - _ -> throwIO e - w <- async $ wsWrite conn - void $ waitAny [r, w] - - wsRead conn = forever $ do - bs <- WS.receiveData conn - case decodeStrict' bs of - Just n -> atomically $ writeTChan eventsChan n - Nothing -> - error $ "Failed to decode events: " ++ show bs - - wsWrite conn = forever $ do - eitherAck <- race (readMVar closeWS) (atomically $ readTChan ackChan) - case eitherAck of - Left () -> WS.sendClose conn (Text.pack "") - Right ack -> WS.sendBinaryData conn (encode ack) - liftIO - $ async - $ WS.runClientWith - caHost - (fromIntegral caPort) - path - WS.defaultConnectionOptions - caHdrs - app diff --git a/integration/test/Testlib/Cannon/ConsumableNotifications.hs b/integration/test/Testlib/Cannon/ConsumableNotifications.hs new file mode 100644 index 00000000000..52a535997fa --- /dev/null +++ b/integration/test/Testlib/Cannon/ConsumableNotifications.hs @@ -0,0 +1,226 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Testlib.Cannon.ConsumableNotifications where + +import Control.Retry +import Data.ByteString.Conversion (toByteString') +import qualified Data.Text as Text +import qualified Network.WebSockets as WS +import Testlib.Prelude hiding (assertNoEvent, awaitAnyEvent, awaitNMatches, awaitNMatchesResult) +import Testlib.Printing +import UnliftIO hiding (handle) + +withEventsWebSockets :: + forall user a. + (HasCallStack, MakesValue user) => + [(user, String)] -> + ([(TChan Value, TChan Value)] -> App a) -> + App a +withEventsWebSockets userClients k = go [] $ reverse userClients + where + go :: [(TChan Value, TChan Value)] -> [(user, String)] -> App a + go chans [] = k chans + go chans ((user, cid) : remaining) = + withEventsWebSocket user cid $ \eventsChan ackChan -> + go ((eventsChan, ackChan) : chans) remaining + +withEventsWebSocket :: (HasCallStack, MakesValue user) => user -> String -> (TChan Value -> TChan Value -> App a) -> App a +withEventsWebSocket user cid k = do + closeWS <- liftIO newEmptyMVar + bracket (setup closeWS) (\(_, _, wsThread) -> liftIO $ cancel wsThread) $ \(eventsChan, ackChan, wsThread) -> do + x <- k eventsChan ackChan + + -- Ensure all the acks are sent before closing the websocket + isAckChanEmpty <- + retrying + (limitRetries 5 <> constantDelay 10_000) + (\_ isEmpty -> pure $ not isEmpty) + (\_ -> liftIO $ atomically $ isEmptyTChan ackChan) + unless isAckChanEmpty $ do + liftIO $ putStrLn $ colored yellow $ "The ack chan is not empty after 50ms, some acks may not make it to the server" + + liftIO $ void $ tryPutMVar closeWS () + + liftIO + $ timeout 1_000_000 (wait wsThread) + >>= \case + Nothing -> + putStrLn $ colored yellow $ "The websocket thread did not close after waiting for 1s" + Just () -> pure () + + pure x + where + setup :: (HasCallStack) => MVar () -> App (TChan Value, TChan Value, Async ()) + setup closeWS = do + (eventsChan, ackChan) <- liftIO $ (,) <$> newTChanIO <*> newTChanIO + wsThread <- eventsWebSocket user cid eventsChan ackChan closeWS + pure (eventsChan, ackChan, wsThread) + +eventsWebSocket :: (MakesValue user) => user -> String -> TChan Value -> TChan Value -> MVar () -> App (Async ()) +eventsWebSocket user clientId eventsChan ackChan closeWS = do + serviceMap <- getServiceMap =<< objDomain user + uid <- objId =<< objQidObject user + let HostPort caHost caPort = serviceHostPort serviceMap Cannon + path = "/events?client=" <> clientId + caHdrs = [(fromString "Z-User", toByteString' uid)] + app conn = do + r <- + async + $ wsRead conn + `catch` \(e :: WS.ConnectionException) -> + case e of + WS.CloseRequest {} -> pure () + _ -> throwIO e + w <- async $ wsWrite conn + void $ waitAny [r, w] + + wsRead conn = forever $ do + bs <- WS.receiveData conn + case decodeStrict' bs of + Just n -> atomically $ writeTChan eventsChan n + Nothing -> + error $ "Failed to decode events: " ++ show bs + + wsWrite conn = forever $ do + eitherAck <- race (readMVar closeWS) (atomically $ readTChan ackChan) + case eitherAck of + Left () -> WS.sendClose conn (Text.pack "") + Right ack -> WS.sendBinaryData conn (encode ack) + liftIO + $ async + $ WS.runClientWith + caHost + (fromIntegral caPort) + path + WS.defaultConnectionOptions + caHdrs + app + +sendMsg :: (HasCallStack) => TChan Value -> Value -> App () +sendMsg eventsChan msg = liftIO $ atomically $ writeTChan eventsChan msg + +ackFullSync :: (HasCallStack) => TChan Value -> App () +ackFullSync ackChan = do + sendMsg ackChan + $ object ["type" .= "ack_full_sync"] + +ackEvent :: (HasCallStack) => TChan Value -> Value -> App () +ackEvent ackChan event = do + deliveryTag <- event %. "data.delivery_tag" + sendAck ackChan deliveryTag False + +sendAck :: (HasCallStack) => TChan Value -> Value -> Bool -> App () +sendAck ackChan deliveryTag multiple = do + sendMsg ackChan + $ object + [ "type" .= "ack", + "data" + .= object + [ "delivery_tag" .= deliveryTag, + "multiple" .= multiple + ] + ] + +assertEvent :: (HasCallStack) => TChan Value -> ((HasCallStack) => Value -> App a) -> App a +assertEvent eventsChan expectations = do + liftIO (timeout 10_000_000 (atomically (readTChan eventsChan))) >>= \case + Nothing -> assertFailure "No event received for 10s" + Just e -> do + pretty <- prettyJSON e + addFailureContext ("event:\n" <> pretty) + $ expectations e + +assertNoEvent :: (HasCallStack) => TChan Value -> App () +assertNoEvent eventsChan = do + timeout 1_000_000 (atomically (readTChan eventsChan)) >>= \case + Nothing -> pure () + Just e -> do + eventJSON <- prettyJSON e + assertFailure $ "Did not expect event: \n" <> eventJSON + +consumeAllEvents :: TChan Value -> TChan Value -> App () +consumeAllEvents eventsChan ackChan = do + timeout 1_000_000 (atomically (readTChan eventsChan)) >>= \case + Nothing -> pure () + Just e -> do + ackEvent ackChan e + consumeAllEvents eventsChan ackChan + +awaitEvent :: TChan Value -> TChan Value -> (Value -> App Bool) -> App Value +awaitEvent eventsChan ackChan selector = do + timeout 10_000_000 (atomically (readTChan eventsChan)) >>= \case + Nothing -> assertFailure "No event received for 10s" + Just e -> do + ackEvent ackChan e + matches <- selector e + if matches + then pure e + else awaitEvent eventsChan ackChan selector + +awaitAnyEvent :: (HasCallStack) => Int -> TChan Value -> App (Maybe Value) +awaitAnyEvent tSecs eventsChan = do + timeout tSecs (atomically (readTChan eventsChan)) + +awaitNMatchesResult :: (HasCallStack) => TChan Value -> Int -> (Value -> App Bool) -> App AwaitResult +awaitNMatchesResult eventsChan n selector = go n [] [] + where + go 0 nonMatches matches = do + refill nonMatches + pure + $ AwaitResult + { success = True, + nMatchesExpected = n, + matches = reverse matches, + nonMatches = reverse nonMatches + } + go nLeft nonMatches matches = do + let tSecs = 10_000_000 + mEvent <- awaitAnyEvent tSecs eventsChan + case mEvent of + Just event -> + do + isMatch <- selector event + if isMatch + then go (nLeft - 1) nonMatches (event : matches) + else go nLeft (event : nonMatches) matches + Nothing -> do + refill nonMatches + pure + $ AwaitResult + { success = False, + nMatchesExpected = n, + matches = reverse matches, + nonMatches = reverse nonMatches + } + refill = mapM_ (liftIO . atomically . writeTChan eventsChan) + +awaitNMatches :: + (HasCallStack) => + -- | Number of matches + Int -> + -- | Selection function. Should not throw any exceptions + (Value -> App Bool) -> + TChan Value -> + App [Value] +awaitNMatches nExpected checkMatch chan = do + res <- awaitNMatchesResult chan nExpected checkMatch + assertAwaitResult res + +awaitMatch :: (HasCallStack) => (Value -> App Bool) -> TChan Value -> App Value +awaitMatch selector chan = do + head <$> awaitNMatches 1 selector chan From bd2fe1ff354542a343be0aea5bf58188ff17dedd Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 26 Nov 2024 10:08:13 +0000 Subject: [PATCH 16/16] wip --- integration/test/MLS/Util.hs | 102 ++++++++++-------- .../test/Performance/BigConversation.hs | 50 ++------- 2 files changed, 68 insertions(+), 84 deletions(-) diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index ed793acfb75..880b983b56a 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -43,6 +43,7 @@ import Testlib.HTTP import Testlib.JSON import Testlib.Prelude import Testlib.Printing +import Data.Time (getCurrentTime, diffUTCTime) mkClientIdentity :: (MakesValue u, MakesValue c) => u -> c -> App ClientIdentity mkClientIdentity u c = do @@ -665,26 +666,27 @@ consumingMessages mlsProtocol mp = Codensity $ \k -> do withWebSockets (map fst clients) $ \wss -> do r <- k () - -- if the conversation is actually MLS (and not mixed), pick one client for - -- each new user and wait for its join event. In Mixed protocol, the user is - -- already in the conversation so they do not get a member-join - -- notification. - when (mlsProtocol == MLSProtocolMLS) $ - traverse_ - (awaitMatch (\n -> isMemberJoinNotif n)) - ( flip Map.restrictKeys newUsers - . Map.mapKeys ((.user) . fst) - . Map.fromList - . toList - $ zip clients wss - ) - - -- at this point we know that every new user has been added to the - -- conversation - for_ (zip clients wss) $ \((cid, t), ws) -> case t of - MLSNotificationMessageTag -> void $ consumeMessageNoExternal conv.ciphersuite cid mp ws - MLSNotificationWelcomeTag -> consumeWelcome cid mp ws - pure r + logTime ("await and consume MLS notifications for " <> show (length clients)) $ do + -- if the conversation is actually MLS (and not mixed), pick one client for + -- each new user and wait for its join event. In Mixed protocol, the user is + -- already in the conversation so they do not get a member-join + -- notification. + when (mlsProtocol == MLSProtocolMLS) $ + traverse_ + (awaitMatch (\n -> isMemberJoinNotif n)) + ( flip Map.restrictKeys newUsers + . Map.mapKeys ((.user) . fst) + . Map.fromList + . toList + $ zip clients wss + ) + + -- at this point we know that every new user has been added to the + -- conversation + for_ (zip clients wss) $ \((cid, t), ws) -> case t of + MLSNotificationMessageTag -> void $ consumeMessageNoExternal conv.ciphersuite cid mp ws + MLSNotificationWelcomeTag -> consumeWelcome cid mp ws + pure r consumeMessageWithPredicate :: (HasCallStack) => (Value -> App Bool) -> ConvId -> Ciphersuite -> ClientIdentity -> Maybe MessagePackage -> WebSocket -> App Value consumeMessageWithPredicate p convId cs cid mmp ws = do @@ -759,32 +761,33 @@ sendAndConsumeCommitBundleWithProtocol protocol mp = do lowerCodensity $ do consumingMessages protocol mp lift $ do - r <- postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 201 - - -- if the sender is a new member (i.e. it's an external commit), then - -- process the welcome message directly - do - conv <- getMLSConv mp.convId - when (Set.member mp.sender conv.newMembers) $ - traverse_ (fromWelcome mp.convId conv.ciphersuite mp.sender) mp.welcome - - -- increment epoch and add new clients - modifyMLSState $ \mls -> - mls - { convs = - Map.adjust - ( \conv -> - conv - { epoch = conv.epoch + 1, - members = conv.members <> conv.newMembers, - newMembers = mempty - } - ) - mp.convId - mls.convs - } + r <- logTime "POST /mls/commit-bundles" $ postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 201 + + logTime "modify local state" $ do + -- if the sender is a new member (i.e. it's an external commit), then + -- process the welcome message directly + do + conv <- getMLSConv mp.convId + when (Set.member mp.sender conv.newMembers) $ + traverse_ (fromWelcome mp.convId conv.ciphersuite mp.sender) mp.welcome + + -- increment epoch and add new clients + modifyMLSState $ \mls -> + mls + { convs = + Map.adjust + ( \conv -> + conv + { epoch = conv.epoch + 1, + members = conv.members <> conv.newMembers, + newMembers = mempty + } + ) + mp.convId + mls.convs + } - pure r + pure r consumeWelcome :: (HasCallStack) => ClientIdentity -> MessagePackage -> WebSocket -> App () consumeWelcome cid mp ws = do @@ -977,3 +980,12 @@ sendAndConsumeCommitBundleWithProtocolNew protocol mp = do } pure r + +logTime :: (HasCallStack) => String -> App a -> App a +logTime desc action = do + start <- liftIO getCurrentTime + res <- action + end <- liftIO getCurrentTime + let diff = diffUTCTime end start + liftIO $ putStrLn $ desc <> " took " <> show diff + pure res diff --git a/integration/test/Performance/BigConversation.hs b/integration/test/Performance/BigConversation.hs index 4bd2b798125..b22da7e697e 100644 --- a/integration/test/Performance/BigConversation.hs +++ b/integration/test/Performance/BigConversation.hs @@ -17,39 +17,12 @@ import UnliftIO (modifyIORef', newIORef, pooledMapConcurrentlyN, readIORef) import UnliftIO.Temporary import Prelude (writeFile) --- | A size saying how big an MLS conversation is. Each size is mapped to a --- number via the 'sizeToNumber' function. -data ConversationSize - = Tiny - | Small - | Medium - | Big - | Large - | VeryLarge - deriving (Eq, Generic, Show) - -sizeToNumber :: ConversationSize -> Word -sizeToNumber Tiny = 20 -sizeToNumber Small = 100 -sizeToNumber Medium = 500 -sizeToNumber Big = 1000 -sizeToNumber Large = 5000 -sizeToNumber VeryLarge = 10000 - -batchForSize :: ConversationSize -> Word -batchForSize Tiny = 10 -batchForSize Small = 20 -batchForSize Medium = 100 -batchForSize Big = 250 -batchForSize Large = 500 -batchForSize VeryLarge = 500 - testCreateBigMLSConversation :: App () testCreateBigMLSConversation = do domain <- OwnDomain & asString - let teamSize = 11 - let batchSize = 20 - let clientNotifCapability = Consumable + let teamSize = 501 + let batchSize = 500 + let clientNotifCapability = Legacy putStrLn $ "Creating a team with " <> show teamSize <> " members" (owner, ownerClient, _, members, c1 : c2 : _) <- createTeamAndClients domain clientNotifCapability teamSize putStrLn $ "Creating a conversation with " <> show teamSize <> " members in batches of " <> show batchSize @@ -59,31 +32,30 @@ testCreateBigMLSConversation = do convId <- createNewGroup def ownerClient let memberChunks = chunksOf batchSize members for_ memberChunks $ \chunk -> do - (size, time) <- timeIt $ do - msg <- createAddCommit ownerClient convId chunk + ((commitSize, bundleSize), time) <- timeIt $ do + msg <- logTime "create add-commit" $ createAddCommit ownerClient convId chunk void $ case clientNotifCapability of Legacy -> sendAndConsumeCommitBundle msg Consumable -> sendAndConsumeCommitBundleNew msg - pure (BS.length msg.message) + pure (BS.length msg.message, BS.length (mkBundle msg)) cs <- liftIO $ readIORef convSize - putStrLn $ "Sent " <> show size <> " bytes in " <> show time <> ", adding " <> show (length chunk) <> " members to conv of size: " <> show cs + putStrLn $ "Sent " <> show commitSize <> " bytes (bundle: " <> show bundleSize <> " bytes) in " <> show time <> ", adding " <> show (length chunk) <> " members to conv of size: " <> show cs liftIO $ modifyIORef' convSize (+ (length chunk)) - pure (size, time) pure convId putStrLn $ "Total time: " <> show totalTime do conv <- getConversation owner (convIdToQidObject convId) >>= getJSON 200 otherMembers <- conv %. "members.others" & asList length otherMembers `shouldMatchInt` (teamSize - 1) - (bytes, timeRemoval) <- timeIt $ do - commit <- createRemoveCommit ownerClient convId [c1, c2] + ((commitSize, bundleSize), time) <- timeIt $ do + commit <- logTime "create removal commit" $ createRemoveCommit ownerClient convId [c1, c2] -- m <- showMessage def ownerClient commit.message -- prettyJSON m >>= liftIO . writeFile "removal.json" case clientNotifCapability of Legacy -> void $ sendAndConsumeCommitBundle commit Consumable -> void $ sendAndConsumeCommitBundleNew commit - pure (BS.length commit.message) - putStrLn $ "Sent " <> show bytes <> " bytes in " <> show timeRemoval <> " for removing 2 members" + pure (BS.length commit.message, BS.length (mkBundle commit)) + putStrLn $ "Sent " <> show commitSize <> " bytes in " <> show timeRemoval <> " for removing 2 members" do conv <- getConversation owner (convIdToQidObject convId) >>= getJSON 200 otherMembers <- conv %. "members.others" & asList