Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,9 @@ public class CustomRedirectCollection : IEnumerable<CustomRedirect>
private readonly IEnumerable<INotFoundHandler> _providers = new List<INotFoundHandler>();

/// <summary>
/// Hashtable for quick lookup of old urls
/// URLs sorted Z-A.
/// </summary>
private readonly Dictionary<string, CustomRedirect> _quickLookupTable = new(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Cache of URLs sorted ZA for look up of partially matched URLs
/// </summary>
private KeyValuePair<string, CustomRedirect>[] _redirectsZACache;
private readonly SortedDictionary<string, CustomRedirect> _redirectsZA = new(new ReverseStringComparer());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching from Dictionary (O(1) lookup) to SortedDictionary (O(log n)) might actually hurt performance, especially since:

  • initialization would take more time and be more processing intensive (as it would have to remain sorted at all times)
  • the main use case involves frequent lookups
  • the performance would be dependent on the collection size

https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2?view=net-9.0
https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.sorteddictionary-2?view=net-9.0


public CustomRedirectCollection()
{
Expand All @@ -39,7 +34,7 @@ public CustomRedirectCollection(IEnumerable<INotFoundHandler> providers)

public IEnumerator<CustomRedirect> GetEnumerator()
{
return _quickLookupTable.Values.GetEnumerator();
return _redirectsZA.Values.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
Expand Down Expand Up @@ -79,34 +74,19 @@ public CustomRedirect Find(Uri urlNotFound)
public void Add(CustomRedirect customRedirect)
{
var oldUrl = HttpUtility.UrlDecode(customRedirect.OldUrl);
if (_quickLookupTable.ContainsKey(oldUrl))
{
_quickLookupTable[oldUrl] = customRedirect;
return;
}

// Add to quick look up table too
_quickLookupTable.Add(oldUrl, customRedirect);

// clean cache
_redirectsZACache = null;
_redirectsZA[oldUrl] = customRedirect;
}

private CustomRedirect FindInternal(string url)
{
url = HttpUtility.UrlDecode(url) ?? string.Empty;
Comment on lines 80 to 82

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The url is decoded here, and then the decoded value is used in _redirectsZA.TryGetValue(url, out var redirect). However, the url is decoded again in the HttpUtility.UrlDecode(customRedirect.OldUrl) call in the Add method. It might be more efficient to decode the URL only once and reuse the decoded value.

if (_quickLookupTable.TryGetValue(url, out var redirect))
if (_redirectsZA.TryGetValue(url, out var redirect))
{
return redirect;
}

// working with local copy to avoid multi-threading issues
var redirectsZA = _redirectsZACache;
if (redirectsZA == null)
{
redirectsZA = _quickLookupTable.OrderByDescending(x => x.Key, StringComparer.OrdinalIgnoreCase).ToArray();
_redirectsZACache = redirectsZA;
}
var redirectsZA = _redirectsZA.ToArray();
Comment on lines 88 to +89

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

While creating a local copy of _redirectsZA using ToArray() addresses some multi-threading concerns, it doesn't fully prevent issues if _redirectsZA is modified after the array is created but before it's used. This could lead to inconsistent results. Consider using a lock or a concurrent collection to ensure thread safety.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This array used to be calculated and cached. With the proposed change, the array is re-created on every call to FindInternal, likely increasing memory usage and garbage collections. FindInternal is expected to be called multiple times during public Find method.


var path = url.AsPathSpan();
var query = url.AsQuerySpan();
Expand Down
27 changes: 27 additions & 0 deletions src/Geta.NotFoundHandler/Core/Redirects/ReverseStringComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Geta Digital. All rights reserved.
// Licensed under Apache-2.0. See the LICENSE file in the project root for more information

using System;
using System.Collections.Generic;

namespace Geta.NotFoundHandler.Core.Redirects;

/// <summary>
/// A comparer that sorts strings in reverse order.
/// </summary>
public class ReverseStringComparer : IComparer<string>
{
private readonly IComparer<string> _baseComparer;

public ReverseStringComparer() : this(StringComparer.OrdinalIgnoreCase) { }

public ReverseStringComparer(IComparer<string> baseComparer)
{
_baseComparer = baseComparer ?? throw new ArgumentNullException(nameof(baseComparer));
}

public int Compare(string x, string y)
{
return _baseComparer.Compare(y, x);
}
}
Loading