diff --git a/common b/common index 6f369214cf..5c6584c29c 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 6f369214cf3c6e44153d2a4eed1ff5b5bbf803a7 +Subproject commit 5c6584c29c1c0b8f5cf675c6f88f16b91e94f552 diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/AcquireTokenFragment.java b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/AcquireTokenFragment.java index aea8d5bbed..dd761f2127 100644 --- a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/AcquireTokenFragment.java +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/AcquireTokenFragment.java @@ -65,6 +65,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Locale; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.StatusCode; @@ -122,6 +123,15 @@ public class AcquireTokenFragment extends Fragment { private MsalWrapper mMsalWrapper; private List mLoadedAccounts = new ArrayList<>(); + // Concurrent execution UI elements + private EditText mConcurrentCount; + private EditText mConcurrentTotalCount; + private Button mRunConcurrent; + private Button mStopConcurrent; + private LinearLayout mThreadProgressContainer; + private List mThreadProgressViews = new ArrayList<>(); + private List mRunningExecutors = new ArrayList<>(); + private IClientActiveBrokerCache mCache; public AcquireTokenFragment() { // left empty @@ -434,6 +444,27 @@ public void onCheckedChanged(CompoundButton v, boolean debugBrokers) { mMsalWrapper.getPreferredAuthMethod() )); + // Initialize concurrent execution UI elements + mConcurrentCount = view.findViewById(R.id.concurrent_count); + mConcurrentTotalCount = view.findViewById(R.id.concurrent_total_count); + mRunConcurrent = view.findViewById(R.id.btn_run_concurrent); + mStopConcurrent = view.findViewById(R.id.btn_stop_concurrent); + mThreadProgressContainer = view.findViewById(R.id.concurrent_thread_progress_container); + + mRunConcurrent.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + runConcurrentAcquireTokenSilent(); + } + }); + + mStopConcurrent.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + stopConcurrentExecutors(); + } + }); + return view; } @@ -694,6 +725,80 @@ public void run() { }); } + /** + * Concurrent AcquireTokenSilent + * Note: In order for this to work, the build config shouldSkipSilentTokenCommandCacheForStressTest must be set to true. + */ + private void runConcurrentAcquireTokenSilent() { + try { + final int concurrency = Integer.parseInt(mConcurrentCount.getText().toString()); + final int totalCount = Integer.parseInt(mConcurrentTotalCount.getText().toString()); + + if (concurrency <= 0 || totalCount <= 0) { + showMessage("Concurrency and total count must be greater than 0"); + return; + } + + // Calculate requests per thread + final int requestsPerThread = totalCount / concurrency; + final int remainder = totalCount % concurrency; + + // Reset progress + mThreadProgressContainer.removeAllViews(); + mThreadProgressViews.clear(); + + // Create per-thread progress views + for (int i = 0; i < concurrency; i++) { + TextView threadProgress = new TextView(getContext()); + threadProgress.setText("Thread " + i + ": 0/0"); + threadProgress.setTextSize(12); + threadProgress.setPadding(0, 5, 0, 5); + mThreadProgressContainer.addView(threadProgress); + mThreadProgressViews.add(threadProgress); + } + + // Create and start one executor per thread + for (int i = 0; i < concurrency; i++) { + final int threadId = i; + final int countForThisThread = requestsPerThread + (i < remainder ? 1 : 0); + + final ConcurrentAcquireTokenExecutor executor = + new ConcurrentAcquireTokenExecutor(threadId, countForThisThread); + + executor.execute( + getContext(), + getCurrentRequestOptions(), + new ConcurrentAcquireTokenExecutor.IUIUpdateCallback() { + @Override + public void updateProgress(final int tid, final int successCount, final int completedCount) { + if (tid >= 0 && tid < mThreadProgressViews.size()) { + mThreadProgressViews.get(tid).setText( + String.format(Locale.US, + "Thread %d: %d/%d", tid, successCount, completedCount)); + } + } + + @Override + public void onStopped(final int tid) { + } + } + ); + + mRunningExecutors.add(executor); + } + } catch (NumberFormatException e) { + showMessage("Please enter valid numbers for concurrency and total count"); + } + } + + private void stopConcurrentExecutors() { + for (ConcurrentAcquireTokenExecutor executor : mRunningExecutors) { + executor.stop(); + } + mRunningExecutors.clear(); + showMessage("Stopped all running tasks"); + } + public interface OnFragmentInteractionListener { void onGetAuthResult(IAuthenticationResult result); diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/ConcurrentAcquireTokenExecutor.kt b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/ConcurrentAcquireTokenExecutor.kt new file mode 100644 index 0000000000..7ef53406b2 --- /dev/null +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/ConcurrentAcquireTokenExecutor.kt @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.client.testapp + +import android.content.Context +import android.os.Handler +import android.os.Looper +import com.microsoft.identity.client.IAuthenticationResult +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean + +class ConcurrentAcquireTokenExecutor( + val threadId: Int, + val totalCount: Int +) { + + private val randomDelayInMs = (10..50).random().toLong() + + // Flag to track if execution should stop + private val isStopped = AtomicBoolean(false) + + // Use a dedicated executor for background work + private val executor: ExecutorService = Executors.newSingleThreadExecutor() + + interface IUIUpdateCallback { + fun updateProgress(threadId: Int, successCount: Int, completedCount: Int) + fun onStopped(threadId: Int) + } + + fun execute(context: Context, + requestOptions: RequestOptions, + uiCallback: IUIUpdateCallback){ + // MsalWrapper.create callbacks run on main thread, so call it directly + MsalWrapper.create( + context.applicationContext, + Constants.getResourceIdFromConfigFile(requestOptions.configFile), + object : INotifyOperationResultCallback { + override fun onSuccess(result: MsalWrapper?) { + // Start the first iteration immediately (no initial sleep) + if (result != null) { + executeAcquireTokenSilent(result, + 0, + 0, + requestOptions, + uiCallback) + } else { + // Handle the null case appropriately, e.g., notify UI or log error + // For now, we'll notify the UI that the operation has stopped + uiCallback.onStopped(threadId) + } + } + + override fun showMessage(message: String?) { + // do nothing. + } + } + ) + } + + /** + * Stop the executor and cancel any pending operations + */ + fun stop() { + if (isStopped.compareAndSet(false, true)) { + executor.shutdownNow() + } + } + + private fun executeAcquireTokenSilent(msalWrapper: MsalWrapper, + successCount: Int, + completedCount: Int, + requestOptions: RequestOptions, + uiCallback: IUIUpdateCallback) { + msalWrapper.acquireTokenSilent( + requestOptions, + object : INotifyOperationResultCallback { + override fun onSuccess(result: IAuthenticationResult) { + val newSuccessCount = successCount + 1 + val newCompletedCount = completedCount + 1 + + // MSAL callbacks already run on main thread - update UI directly + uiCallback.updateProgress(threadId, newSuccessCount, newCompletedCount) + executeNext( + msalWrapper, + newSuccessCount, + newCompletedCount, + requestOptions, + uiCallback + ) + } + + override fun showMessage(message: String?) { + val newCompletedCount = completedCount + 1 + + // MSAL callbacks already run on main thread - update UI directly + uiCallback.updateProgress(threadId, successCount, newCompletedCount) + + executeNext( + msalWrapper, + successCount, + newCompletedCount, + requestOptions, + uiCallback + ) + } + } + ) + } + + private fun executeNext( + msalWrapper: MsalWrapper, + newSuccessCount: Int, + newCompletedCount: Int, + requestOptions: RequestOptions, + uiCallback: IUIUpdateCallback + ) { + // Check if stopped + if (isStopped.get()) { + Handler(Looper.getMainLooper()).post { + uiCallback.onStopped(threadId) + } + return + } + + if (newCompletedCount < totalCount) { + executor.execute { + try { + Thread.sleep(randomDelayInMs) + + // Post back to main thread to call acquireTokenSilent + // (required since MSAL needs to be called from main thread) + Handler(Looper.getMainLooper()).post { + executeAcquireTokenSilent( + msalWrapper, + newSuccessCount, + newCompletedCount, + requestOptions, + uiCallback + ) + } + } catch (e: InterruptedException) { + // Interrupted - likely due to stop request + Handler(Looper.getMainLooper()).post { + uiCallback.onStopped(threadId) + } + } + } + } + } + +} \ No newline at end of file diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/RequestOptions.java b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/RequestOptions.java index ce6b332381..82b0a75d85 100644 --- a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/RequestOptions.java +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/RequestOptions.java @@ -35,7 +35,7 @@ @Getter @Accessors(prefix = "m") @AllArgsConstructor -class RequestOptions { +public class RequestOptions { private final Constants.ConfigFile mConfigFile; private final String mLoginHint; private final IAccount mAccount; @@ -52,4 +52,8 @@ class RequestOptions { private final String mPopResourceUrl; private final String mPoPClientClaims; private final boolean mAllowSignInFromOtherDevice; + + public Constants.ConfigFile getConfigFile(){ + return mConfigFile; + } } diff --git a/testapps/testapp/src/main/res/layout/fragment_acquire.xml b/testapps/testapp/src/main/res/layout/fragment_acquire.xml index 851ac889e4..1a30403bfd 100644 --- a/testapps/testapp/src/main/res/layout/fragment_acquire.xml +++ b/testapps/testapp/src/main/res/layout/fragment_acquire.xml @@ -661,8 +661,7 @@ android:layout_height="wrap_content" android:orientation="horizontal" android:paddingTop="5dp" - android:paddingBottom="5dp" - android:weightSum="10"> + android:paddingBottom="5dp">