Skip to content

Commit

Permalink
[Token Cache] Add perf tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
pmaytak authored and bgavrilMS committed Sep 29, 2021
1 parent 5fe3e79 commit 7952732
Show file tree
Hide file tree
Showing 18 changed files with 1,613 additions and 929 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ internal interface ITokenCacheAccessor
/// </summary>
/// <remarks>
/// WARNING: if partitionKey is null, this API is slow as it loads all tokens, not just from 1 partition.
/// It should only to support external token caching, in the hope that the external token cache is partitioned.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
/// Not all classes that implement this method are required to filter by partition (e.g. mobile)
/// </remarks>
IReadOnlyList<MsalAccessTokenCacheItem> GetAllAccessTokens(string optionalPartitionKey = null);
Expand All @@ -50,7 +50,7 @@ internal interface ITokenCacheAccessor
/// </summary>
/// <remarks>
/// WARNING: if partitionKey is null, this API is slow as it loads all tokens, not just from 1 partition.
/// It should only to support external token caching, in the hope that the external token cache is partitioned.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
/// Not all classes that implement this method are required to filter by partition (e.g. mobile)
/// </remarks>
IReadOnlyList<MsalRefreshTokenCacheItem> GetAllRefreshTokens(string optionalPartitionKey = null);
Expand All @@ -61,7 +61,7 @@ internal interface ITokenCacheAccessor
/// </summary>
/// <remarks>
/// WARNING: if partitionKey is null, this API is slow as it loads all tokens, not just from 1 partition.
/// It should only to support external token caching, in the hope that the external token cache is partitioned.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
/// Not all classes that implement this method are required to filter by partition (e.g. mobile)
/// </remarks>
IReadOnlyList<MsalIdTokenCacheItem> GetAllIdTokens(string optionalPartitionKey = null);
Expand All @@ -72,7 +72,7 @@ internal interface ITokenCacheAccessor
/// </summary>
/// <remarks>
/// WARNING: if partitionKey is null, this API is slow as it loads all tokens, not just from 1 partition.
/// It should only to support external token caching, in the hope that the external token cache is partitioned.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
/// Not all classes that implement this method are required to filter by partition (e.g. mobile)
/// </remarks>
IReadOnlyList<MsalAccountCacheItem> GetAllAccounts(string optionalPartitionKey = null);
Expand All @@ -87,7 +87,7 @@ internal interface ITokenCacheAccessor

/// <remarks>
/// WARNING: this API is slow as it loads all tokens, not just from 1 partition.
/// It should only to support external token caching, in the hope that the external token cache is partitioned.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
/// </remarks>
bool HasAccessOrRefreshTokens();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ public void DeleteAccount(MsalAccountCacheItem item)

/// <summary>
/// WARNING: if partitonKey = null, this API is slow as it loads all tokens, not just from 1 partition.
/// It should only to support external token caching, in the hope that the external token cache is partitioned.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
/// </summary>
public virtual IReadOnlyList<MsalAccessTokenCacheItem> GetAllAccessTokens(string partitionKey = null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public void SaveRefreshToken(MsalRefreshTokenCacheItem item)
{
string itemKey = item.GetKey().ToString();
string partitionKey = CacheKeyFactory.GetKeyFromCachedItem(item);

RefreshTokenCacheDictionary
.GetOrAdd(partitionKey, new ConcurrentDictionary<string, MsalRefreshTokenCacheItem>())[itemKey] = item;
}
Expand Down Expand Up @@ -178,7 +179,7 @@ public void DeleteAccount(MsalAccountCacheItem item)
#region Get All

/// WARNING: if partitionKey is null, this API is slow as it loads all tokens, not just from 1 partition.
/// It should only to support external token caching, in the hope that the external token cache is partitioned.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
public virtual IReadOnlyList<MsalAccessTokenCacheItem> GetAllAccessTokens(string partitionKey = null)
{
if (string.IsNullOrEmpty(partitionKey))
Expand All @@ -193,7 +194,7 @@ public virtual IReadOnlyList<MsalAccessTokenCacheItem> GetAllAccessTokens(string
}

/// WARNING: if partitionKey is null, this API is slow as it loads all tokens, not just from 1 partition.
/// It should only to support external token caching, in the hope that the external token cache is partitioned.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
public virtual IReadOnlyList<MsalRefreshTokenCacheItem> GetAllRefreshTokens(string partitionKey = null)
{
if (string.IsNullOrEmpty(partitionKey))
Expand All @@ -208,7 +209,7 @@ public virtual IReadOnlyList<MsalRefreshTokenCacheItem> GetAllRefreshTokens(stri
}

/// WARNING: if partitionKey is null, this API is slow as it loads all tokens, not just from 1 partition.
/// It should only to support external token caching, in the hope that the external token cache is partitioned.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
public virtual IReadOnlyList<MsalIdTokenCacheItem> GetAllIdTokens(string partitionKey = null)
{
if (string.IsNullOrEmpty(partitionKey))
Expand All @@ -223,7 +224,7 @@ public virtual IReadOnlyList<MsalIdTokenCacheItem> GetAllIdTokens(string partiti
}

/// WARNING: if partitionKey is null, this API is slow as it loads all tokens, not just from 1 partition.
/// It should only to support external token caching, in the hope that the external token cache is partitioned.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
public virtual IReadOnlyList<MsalAccountCacheItem> GetAllAccounts(string partitionKey = null)
{
if (string.IsNullOrEmpty(partitionKey))
Expand Down Expand Up @@ -258,7 +259,7 @@ public virtual void Clear()
}

/// WARNING: this API is slow as it loads all tokens, not just from 1 partition.
/// It should only to support external token caching, in the hope that the external token cache is partitioned.
/// It should only support external token caching, in the hope that the external token cache is partitioned.
public virtual bool HasAccessOrRefreshTokens()
{
return RefreshTokenCacheDictionary.Any(partition => partition.Value.Count > 0) ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ public sealed partial class TokenCache : ITokenCacheInternal
if (!string.IsNullOrEmpty(response.RefreshToken))
{
Debug.Assert(
requestParams.ApiId != ApiEvent.ApiIds.AcquireTokenForClient,
"client_credentials flow should receive RT");
requestParams.ApiId != ApiEvent.ApiIds.AcquireTokenForClient,
"client_credentials flow should not receive a refresh token");

msalRefreshTokenCacheItem = new MsalRefreshTokenCacheItem(
instanceDiscoveryMetadata.PreferredCache,
Expand All @@ -103,7 +103,7 @@ public sealed partial class TokenCache : ITokenCacheInternal
{
Debug.Assert(
requestParams.ApiId != ApiEvent.ApiIds.AcquireTokenForClient,
"client_credentials flow should receive IdToken");
"client_credentials flow should not receive an ID token");

msalIdTokenCacheItem = new MsalIdTokenCacheItem(
instanceDiscoveryMetadata.PreferredCache,
Expand Down Expand Up @@ -406,7 +406,7 @@ private static string GetPreferredUsernameFromIdToken(bool isAdfsAuthority, IdTo
// take a snapshot of the access tokens to avoid problems where the underlying collection is changed,
// as this method is NOT locked by the semaphore
string partitionKey = CacheKeyFactory.GetKeyFromRequest(requestParams);
Debug.Assert(partitionKey != null || !requestParams.IsConfidentialClient, "On confidential client, cache must be partition");
Debug.Assert(partitionKey != null || !requestParams.IsConfidentialClient, "On confidential client, cache must be partitioned.");

IReadOnlyList<MsalAccessTokenCacheItem> tokenCacheItems = GetAllAccessTokensWithNoLocks(true, partitionKey);
if (tokenCacheItems.Count == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,28 @@ internal static class TokenCacheHelper
ClientId = TestConstants.ClientId,
Environment = TestConstants.ProductionPrefCacheEnvironment,
HomeAccountId = homeAccountId,
UserAssertionHash = userAssertionHash,
UserAssertionHash = userAssertionHash,
Secret = string.Empty
};
}

internal static MsalIdTokenCacheItem CreateIdTokenCacheItem(
string tenant = TestConstants.TenantId,
string homeAccountId = TestConstants.HomeAccountId)
string tenant = TestConstants.Utid,
string homeAccountId = TestConstants.HomeAccountId,
string uid = TestConstants.Uid)
{
return new MsalIdTokenCacheItem()
{
ClientId = TestConstants.ClientId,
Environment = TestConstants.ProductionPrefCacheEnvironment,
HomeAccountId = homeAccountId,
TenantId = tenant,
Secret = MockHelpers.CreateIdToken(uid, TestConstants.DisplayableId, tenant)
};
}

internal static MsalAccountCacheItem CreateAccountItem(
string tenant = TestConstants.TenantId,
string tenant = TestConstants.Utid,
string homeAccountId = TestConstants.HomeAccountId)
{
return new MsalAccountCacheItem()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Microsoft.Identity.Client;
Expand All @@ -14,8 +15,10 @@ namespace Microsoft.Identity.Test.Performance
/// Used to test the performance of token cache with large amount of items.
/// </summary>
/// <remarks>
/// For app cache, the number of partitions is the number of tenants
///
/// Testing combinations
/// Tenants - Tokens - Total tokens
/// Tenants (partitions) - Tokens per partition - Total tokens
/// 1 - 10,000 - 10,000
/// 1 - 100,000 - 100,000
/// 100 - 10,000 - 1,000,000
Expand All @@ -25,44 +28,48 @@ namespace Microsoft.Identity.Test.Performance
[MeanColumn, StdDevColumn, MedianColumn, MinColumn, MaxColumn]
public class AcquireTokenForClientLargeCacheTests
{
readonly string _tenantPrefix = "tid";
readonly string _tenantPrefix = TestConstants.Utid;
readonly string _scopePrefix = "scope";
ConfidentialClientApplication _cca;
string _tenantId = "tid";
string _scope = "scope";
string _tenantId;
string _scope;

[Params(1000, Priority = 0)]
public int Tenants { get; set; }
[ParamsSource(nameof(CacheSizeSource))]
public (int Tenants, int TokensPerTenant) CacheSize { get; set; }

[Params(1000, Priority = 1)]
public int TokensPerTenant { get; set; }
// By default, benchmarks are run for all combinations of params.
// This is a workaround to specify the exact param combinations to be used.
public IEnumerable<(int, int)> CacheSizeSource => new[] {
(1, 10000),
(1, 100000),
(100, 10000),
(1000, 1000),
(10000, 100) };

[GlobalSetup]
public void GlobalSetup()
{
_cca = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithRedirectUri(TestConstants.RedirectUri)
.WithClientSecret(TestConstants.ClientSecret)
.BuildConcrete();

PopulateAppCache(_cca, Tenants, TokensPerTenant);
PopulateAppCache(_cca, CacheSize.Tenants, CacheSize.TokensPerTenant);
}

[IterationSetup]
public void IterationSetup()
{
Random random = new Random();
_tenantId = $"{_tenantPrefix}{random.Next(0, Tenants)}";
_scope = $"{_scopePrefix}{random.Next(0, TokensPerTenant)}";
_tenantId = $"{_tenantPrefix}{random.Next(0, CacheSize.Tenants)}";
_scope = $"{_scopePrefix}{random.Next(0, CacheSize.TokensPerTenant)}";
}

[Benchmark]
public async Task AcquireTokenForClient_TestAsync()
public async Task<AuthenticationResult> AcquireTokenForClient_TestAsync()
{
var result = await _cca.AcquireTokenForClient(new[] { _scope })
.WithForceRefresh(false)
return await _cca.AcquireTokenForClient(new[] { _scope })
.WithAuthority($"https://login.microsoftonline.com/{_tenantId}")
.ExecuteAsync()
.ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ namespace Microsoft.Identity.Test.Performance
/// 10,000 - 100 - 1,000,000
/// </remarks>
[MeanColumn, StdDevColumn, MedianColumn, MinColumn, MaxColumn]
public class AcquireTokenForOboCacheTests
public class AcquireTokenForOboLargeCacheTests
{
readonly string _scopePrefix = "scope";
readonly string _tenantPrefix = TestConstants.Utid;
Expand Down
2 changes: 1 addition & 1 deletion tests/Microsoft.Identity.Test.Performance/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run<AcquireTokenForOboCacheTests>(
BenchmarkRunner.Run<TokenCacheTests>(
DefaultConfig.Instance
.WithOptions(ConfigOptions.DontOverwriteResults)
.AddDiagnoser(MemoryDiagnoser.Default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,30 +41,6 @@ public async Task CCAFlows_CachePartition_TestAsync()
}
}

[TestMethod]
public async Task PCAFlows_CachePartition_TestAsync()
{
using (var harness = base.CreateTestHarness())
{
var httpManager = harness.HttpManager;
httpManager.AddInstanceDiscoveryMockHandler();

var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId)
.WithRedirectUri(TestConstants.RedirectUri)
.WithClientSecret(TestConstants.ClientSecret)
.WithHttpManager(httpManager)
// this will fail if cache partitioning rules are broken (but not when cache serialization is also used)
.WithCachePartitioningAsserts(harness.ServiceBundle.PlatformProxy)
.BuildConcrete();

await RunClientCreds_Async(httpManager, app).ConfigureAwait(false);

await RunObo_Async(httpManager, app).ConfigureAwait(false);

await RunAuthCode_Async(httpManager, app).ConfigureAwait(false);
}
}

private static async Task RunAuthCode_Async(MockHttpManager httpManager, ConfidentialClientApplication app)
{
httpManager.AddSuccessTokenResponseMockHandlerForPost();
Expand Down
18 changes: 18 additions & 0 deletions tests/devapps/DesktopTestApp/DesktopTestApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@
<Compile Include="MainForm.Designer.cs">
<DependentUpon>MainForm.cs</DependentUpon>
</Compile>
<Compile Include="MsalUserAccessTokenControl.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="MsalUserAccessTokenControl.Designer.cs">
<DependentUpon>MsalUserAccessTokenControl.cs</DependentUpon>
</Compile>
<Compile Include="MsalUserRefreshTokenControl.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="MsalUserRefreshTokenControl.Designer.cs">
<DependentUpon>MsalUserRefreshTokenControl.cs</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="PublicClientHandler.cs" />
Expand All @@ -67,6 +79,12 @@
<EmbeddedResource Include="MainForm.resx">
<DependentUpon>MainForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="MsalUserAccessTokenControl.resx">
<DependentUpon>MsalUserAccessTokenControl.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="MsalUserRefreshTokenControl.resx">
<DependentUpon>MsalUserRefreshTokenControl.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
Expand Down
Loading

0 comments on commit 7952732

Please sign in to comment.