Skip to content

Commit

Permalink
Create cache to speed up case-insensitive look up of schema elements (#…
Browse files Browse the repository at this point in the history
…2610)

Create NormalizedSchemaElementsCache to speed-up case-insensitive look up of schema elements in ODataUriResolver.
  • Loading branch information
habbes authored Feb 9, 2023
1 parent 8ca4e43 commit 708384e
Show file tree
Hide file tree
Showing 8 changed files with 940 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//---------------------------------------------------------------------
// <copyright file="NormalizedSchemaElementsCache.cs" company="Microsoft">
// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
// </copyright>
//---------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.OData.Edm.Vocabularies;

namespace Microsoft.OData.Edm
{
/// <summary>
/// Cache used to store schema elements using case-normalized names
/// to speed up case-insensitive model lookups. The cache is populated
/// up front so that if an item is not found in the cache, we can assume
/// it doesn't exist in the model. For this reason, it's important that
/// the model be immutable.
/// </summary>
internal sealed class NormalizedSchemaElementsCache
{
// We create different caches for different types of schema elements because all current usage request schema elements
// of specific types. If we were to use a single dictionary <string, ISchemaElement> we would need
// to do additional work (and allocations) during lookups to filter the results to the susbset that matches the request type.
private readonly Dictionary<string, List<IEdmSchemaType>> schemaTypesCache = new Dictionary<string, List<IEdmSchemaType>>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, List<IEdmOperation>> operationsCache = new Dictionary<string, List<IEdmOperation>>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, List<IEdmTerm>> termsCache = new Dictionary<string, List<IEdmTerm>>(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Builds a case-insensitive cache of schema elements from
/// the specified <paramref name="model"/>.
/// </summary>
/// <param name="model">The model whose schema elements to cache. This model should be immutable. See <see cref="ExtensionMethods.MarkAsImmutable(IEdmModel)"/>.</param>
public NormalizedSchemaElementsCache(IEdmModel model)
{
Debug.Assert(model != null);

PopulateSchemaElements(model);

foreach (IEdmModel referencedModel in model.ReferencedModels)
{
PopulateSchemaElements(referencedModel);
}
}

/// <summary>
/// Find all schema types that match the <paramref name="qualifiedName"/>.
/// </summary>
/// <param name="qualifiedName">The case-insensitive fully qualified name to match.</param>
/// <returns>A list of matching schema types, or null if no schema type matches.</returns>
public List<IEdmSchemaType> FindSchemaTypes(string qualifiedName)
{
if (schemaTypesCache.TryGetValue(qualifiedName, out List<IEdmSchemaType> results))
{
return results;
}

return null;
}

/// <summary>
/// Find all operations that match the <paramref name="qualifiedName"/>.
/// </summary>
/// <param name="qualifiedName">The case-insensitive fully qualified name to match.</param>
/// <returns>A list of matching operations, or null if no operation matches.</returns>
public List<IEdmOperation> FindOperations(string qualifiedName)
{
if (operationsCache.TryGetValue(qualifiedName, out List<IEdmOperation> results))
{
return results;
}

return null;
}

/// <summary>
/// Find all vocabulary terms that match the <paramref name="qualifiedName"/>.
/// </summary>
/// <param name="qualifiedName">The case-insensitive fully qualified name to match.</param>
/// <returns>A list of matching terms, or null if no operation matches.</returns>
public List<IEdmTerm> FindTerms(string qualifiedName)
{
if (termsCache.TryGetValue(qualifiedName, out List<IEdmTerm> results))
{
return results;
}

return null;
}

private void PopulateSchemaElements(IEdmModel model)
{
foreach (IEdmSchemaElement element in model.SchemaElements)
{
if (element is IEdmSchemaType schemaType)
{
AddElementToCache(schemaType, schemaTypesCache);
}
else if (element is IEdmOperation operation)
{
AddElementToCache(operation, operationsCache);
}
else if (element is IEdmTerm term)
{
AddElementToCache(term, termsCache);
}
}
}

private static void AddElementToCache<T>(T element, Dictionary<string, List<T>> cache) where T : IEdmSchemaElement
{
List<T> results;
string normalizedKey = element.FullName();
if (!cache.TryGetValue(normalizedKey, out results))
{
results = new List<T>();
cache[normalizedKey] = results;
}

results.Add(element);
}
}
}
59 changes: 47 additions & 12 deletions src/Microsoft.OData.Core/UriParser/Resolver/ODataUriResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@
//---------------------------------------------------------------------

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics;
using System.Linq;
using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Vocabularies;
using Microsoft.OData.Edm.Vocabularies.V1;

namespace Microsoft.OData.UriParser
{
Expand Down Expand Up @@ -167,7 +165,7 @@ public virtual IEdmTerm ResolveTerm(IEdmModel model, string termName)
return term;
}

IList<IEdmTerm> results = FindAcrossModels<IEdmTerm>(model, termName, /*caseInsensitive*/ true);
IReadOnlyList<IEdmTerm> results = FindSchemaElements(model, termName, (cache, key) => cache.FindTerms(key));

if (results == null || results.Count == 0)
{
Expand Down Expand Up @@ -196,7 +194,7 @@ public virtual IEdmSchemaType ResolveType(IEdmModel model, string typeName)
return type;
}

IList<IEdmSchemaType> results = FindAcrossModels<IEdmSchemaType>(model, typeName, /*caseInsensitive*/ true);
IReadOnlyList<IEdmSchemaType> results = FindSchemaElements(model, typeName, (cache, key) => cache.FindSchemaTypes(key));

if (results == null || results.Count == 0)
{
Expand Down Expand Up @@ -226,7 +224,8 @@ public virtual IEnumerable<IEdmOperation> ResolveBoundOperations(IEdmModel model
return results;
}

IList<IEdmOperation> operations = FindAcrossModels<IEdmOperation>(model, identifier, /*caseInsensitive*/ true);
IReadOnlyList<IEdmOperation> operations = FindSchemaElements(model, identifier, (cache, key) => cache.FindOperations(key));

if (operations != null && operations.Count > 0)
{
IList<IEdmOperation> matchedOperation = new List<IEdmOperation>();
Expand Down Expand Up @@ -258,7 +257,8 @@ public virtual IEnumerable<IEdmOperation> ResolveUnboundOperations(IEdmModel mod
return results;
}

IList<IEdmOperation> operations = FindAcrossModels<IEdmOperation>(model, identifier, /*caseInsensitive*/ true);
IReadOnlyList<IEdmOperation> operations = FindSchemaElements(model, identifier, (cache, key) => cache.FindOperations(key));

if (operations != null && operations.Count > 0)
{
IList<IEdmOperation> matchedOperation = new List<IEdmOperation>();
Expand Down Expand Up @@ -504,20 +504,34 @@ internal static ODataUriResolver GetUriResolver(IServiceProvider container)
return container.GetRequiredService<ODataUriResolver>();
}

private static IList<T> FindAcrossModels<T>(IEdmModel model, String qualifiedName, bool caseInsensitive) where T : IEdmSchemaElement
private static IReadOnlyList<T> FindSchemaElements<T>(
IEdmModel model,
string identifier,
Func<NormalizedSchemaElementsCache, string, List<T>> cacheLookupFunc) where T : IEdmSchemaElement
{
if (model.IsImmutable())
{
NormalizedSchemaElementsCache cache = GetNormalizedSchemaElementsCache(model);
return cacheLookupFunc(cache, identifier);
}

return FindAcrossModels<T>(model, identifier, caseInsensitive: true);
}

private static IReadOnlyList<T> FindAcrossModels<T>(IEdmModel model, string qualifiedName, bool caseInsensitive) where T : IEdmSchemaElement
{
IList<T> results = new List<T>();
FindSchemaElements<T>(model, qualifiedName, caseInsensitive, ref results);
FindSchemaElementsInModel<T>(model, qualifiedName, caseInsensitive, ref results);

foreach (IEdmModel reference in model.ReferencedModels)
{
FindSchemaElements<T>(reference, qualifiedName, caseInsensitive, ref results);
FindSchemaElementsInModel<T>(reference, qualifiedName, caseInsensitive, ref results);
}

return results;
return results as IReadOnlyList<T>;
}

private static void FindSchemaElements<T>(IEdmModel model, string qualifiedName, bool caseInsensitive, ref IList<T> results) where T : IEdmSchemaElement
private static void FindSchemaElementsInModel<T>(IEdmModel model, string qualifiedName, bool caseInsensitive, ref IList<T> results) where T : IEdmSchemaElement
{
foreach (IEdmSchemaElement schema in model.SchemaElements)
{
Expand All @@ -530,5 +544,26 @@ private static void FindSchemaElements<T>(IEdmModel model, string qualifiedName,
}
}
}

private static NormalizedSchemaElementsCache GetNormalizedSchemaElementsCache(IEdmModel model)
{
Debug.Assert(model != null);

NormalizedSchemaElementsCache cache = model.GetAnnotationValue<NormalizedSchemaElementsCache>(model);
if (cache == null)
{
// There's a chance 2 or more threads can reach here concurrently
// for the first N parallel requests, and each will build the cache.
// While that is wasteful, it doesn't affect the behaviour of the cache since the model is immutable.
// The last cache to be attached to the model will "win" and be used for all subsequent requests.
// We can avoid this waste by providing a method that user can call manually to build
// the cache before any request is made. But I did not want to add a new method to the public API.
// We revisit this if it turns out to be a problem in practice.
cache = new NormalizedSchemaElementsCache(model);
model.SetAnnotationValue(model, cache);
}

return cache;
}
}
}
Loading

0 comments on commit 708384e

Please sign in to comment.