Skip to content

Commit

Permalink
Threading/performance fixes - delegate value factory invocation guara…
Browse files Browse the repository at this point in the history
…nteed to be atomic (due to that amount of computations related to redundant parsing are decreased).
  • Loading branch information
jwaliszko committed Aug 20, 2016
1 parent 49f225e commit 8c3cd58
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 82 deletions.
3 changes: 2 additions & 1 deletion src/ExpressiveAnnotations.MvcUnobtrusive.Tests/BaseTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using ExpressiveAnnotations.MvcUnobtrusive.Caching;
using Moq;

namespace ExpressiveAnnotations.MvcUnobtrusive.Tests
Expand All @@ -17,7 +18,7 @@ public BaseTest()
new HttpResponse(new StringWriter())
);

MapCache.Clear();
MapCache<string, CacheItem>.Clear();
}

protected ModelMetadata GetModelMetadata<TModel, TProp>(TModel model, Expression<Func<TModel, TProp>> expression)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
</Choose>
<ItemGroup>
<Compile Include="BaseTest.cs" />
<Compile Include="MapCacheTest.cs" />
<Compile Include="Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
Expand Down
110 changes: 110 additions & 0 deletions src/ExpressiveAnnotations.MvcUnobtrusive.Tests/MapCacheTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Castle.Core.Internal;
using ExpressiveAnnotations.MvcUnobtrusive.Caching;
using Xunit;

namespace ExpressiveAnnotations.MvcUnobtrusive.Tests
{
public class MapCacheTest
{
public MapCacheTest()
{
MapCache<string, TestItem>.Clear();
}

[Fact]
public void verify_bahaviour_for_concurrent_access_under_different_keys()
{
var keys = new[] {"a", "b"};
var counter = new ConcurrentStack<int>(); // value factory threads
var storage = new ConcurrentStack<TestItem>(); // cached items

// first run
var threads = MakeThreads(keys);
threads.ForEach(t => t.Start(new object[] {storage, counter}));
threads.ForEach(t => t.Join());

Assert.Equal(2, counter.Count);
Assert.Equal(2, storage.Count);
Assert.NotSame(storage.First(), storage.Last());
var a = storage.FirstOrDefault(x => x.Id == "a");
var b = storage.FirstOrDefault(x => x.Id == "b");

// cleanups and second run
storage.Clear();
counter.Clear();

threads = MakeThreads(keys);
threads.ForEach(t => t.Start(new object[] {storage, counter}));
threads.ForEach(t => t.Join());

Assert.Equal(0, counter.Count);
Assert.Equal(2, storage.Count);
Assert.NotSame(storage.First(), storage.Last());
var aa = storage.FirstOrDefault(x => x.Id == "a");
var bb = storage.FirstOrDefault(x => x.Id == "b");
Assert.Same(a, aa);
Assert.Same(b, bb);
}

[Fact]
public void verify_bahaviour_for_concurrent_access_under_identical_keys()
{
var keys = new[] {"a", "a"};
var counter = new ConcurrentStack<int>();
var storage = new ConcurrentStack<TestItem>();

// first run
var threads = MakeThreads(keys);
threads.ForEach(t => t.Start(new object[] {storage, counter}));
threads.ForEach(t => t.Join());

Assert.Equal(1, counter.Count);
Assert.Equal(2, storage.Count);
var a = storage.First();
Assert.Same(storage.First(), storage.Last());

// cleanups and second run
storage.Clear();
counter.Clear();

threads = MakeThreads(keys);
threads.ForEach(t => t.Start(new object[] {storage, counter}));
threads.ForEach(t => t.Join());

Assert.Equal(0, counter.Count);
Assert.Equal(2, storage.Count);
var aa = storage.First();
Assert.Same(storage.First(), storage.Last());
Assert.Same(a, aa);
}

private Thread[] MakeThreads(string[] keys)
{
var threads = keys.Select(key =>
new Thread(load =>
{
var storage = (ConcurrentStack<TestItem>) ((object[]) load)[0];
var counter = (ConcurrentStack<int>) ((object[]) load)[1];

var item = MapCache<string, TestItem>.GetOrAdd(key.ToString(), _ =>
{
Debug.WriteLine($"{key} :: {Thread.CurrentThread.ManagedThreadId}");
counter.Push(Thread.CurrentThread.ManagedThreadId); // we want to test that this value factory delegate is invoked only once, even if map is accessed concurrently for the same key
Thread.Sleep(500);
return new TestItem {Id = key};
});
storage.Push(item);
})).ToArray();
return threads;
}

private class TestItem
{
public string Id { get; set; }
}
}
}
41 changes: 41 additions & 0 deletions src/ExpressiveAnnotations.MvcUnobtrusive/Caching/MapCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* https://github.com/jwaliszko/ExpressiveAnnotations
* Copyright (c) 2014 Jarosław Waliszko
* Licensed MIT: http://opensource.org/licenses/MIT */

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;

namespace ExpressiveAnnotations.MvcUnobtrusive.Caching
{
/// <summary>
/// Persists decomposed expressions parts for entire application instance. Implementation is concurrent and lazy.
/// </summary>
internal static class MapCache<TKey, TValue> // http://stackoverflow.com/q/3037203/270315
{
private static readonly ConcurrentDictionary<TKey, Lazy<TValue>> _cache = new ConcurrentDictionary<TKey, Lazy<TValue>>(); // why lazy? -> http://stackoverflow.com/q/12611167/270315, https://blogs.endjin.com/2015/10/using-lazy-and-concurrentdictionary-to-ensure-a-thread-safe-run-once-lazy-loaded-collection/

public static TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory) // delegate value factory invocation guaranteed to be atomic
{
var lazyResult = _cache.GetOrAdd(
key,
k => new Lazy<TValue>(
() => valueFactory(k),
LazyThreadSafetyMode.ExecutionAndPublication));
return lazyResult.Value;
}

public static void Clear()
{
_cache.Clear();
}
}

internal class CacheItem
{
public IDictionary<string, string> FieldsMap { get; set; }
public IDictionary<string, object> ConstsMap { get; set; }
public IDictionary<string, string> ParsersMap { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,43 +1,43 @@
/* https://github.com/jwaliszko/ExpressiveAnnotations
* Copyright (c) 2014 Jarosław Waliszko
* Licensed MIT: http://opensource.org/licenses/MIT */

using System;
using System.Collections;
using System.Web;

namespace ExpressiveAnnotations.MvcUnobtrusive
{
/// <summary>
/// Persists arbitrary data for the current HTTP request.
/// </summary>
internal static class RequestStorage
{
private static IDictionary Items
{
get
{
if (HttpContext.Current == null)
throw new ApplicationException("HttpContext not available.");
return HttpContext.Current.Items; // location that could be used throughtout the entire HTTP request lifetime
} // (contrary to a session, this one exists only within the period of a single request).
}

public static T Get<T>(string key)
{
return Items[key] == null
? default(T)
: (T) Items[key];
}

public static void Set<T>(string key, T value)
{
Items[key] = value;
}

public static void Remove(string key)
{
Items.Remove(key);
}
}
}
/* https://github.com/jwaliszko/ExpressiveAnnotations
* Copyright (c) 2014 Jarosław Waliszko
* Licensed MIT: http://opensource.org/licenses/MIT */

using System;
using System.Collections;
using System.Web;

namespace ExpressiveAnnotations.MvcUnobtrusive.Caching
{
/// <summary>
/// Persists arbitrary data for the current HTTP request.
/// </summary>
internal static class RequestStorage
{
private static IDictionary Items
{
get
{
if (HttpContext.Current == null)
throw new ApplicationException("HttpContext not available.");
return HttpContext.Current.Items; // location that could be used throughtout the entire HTTP request lifetime
} // (contrary to a session, this one exists only within the period of a single request).
}

public static T Get<T>(string key)
{
return Items[key] == null
? default(T)
: (T) Items[key];
}

public static void Set<T>(string key, T value)
{
Items[key] = value;
}

public static void Remove(string key)
{
Items.Remove(key);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Providers\ExpressiveAnnotationsModelValidatorProvider.cs" />
<Compile Include="RequestStorage.cs" />
<Compile Include="MapCache.cs" />
<Compile Include="Caching\RequestStorage.cs" />
<Compile Include="Caching\MapCache.cs" />
<Compile Include="Validators\AssertThatValidator.cs" />
<Compile Include="Helper.cs" />
<Compile Include="Validators\ExpressiveValidator.cs" />
Expand Down
35 changes: 0 additions & 35 deletions src/ExpressiveAnnotations.MvcUnobtrusive/MapCache.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Web.Mvc;
using ExpressiveAnnotations.Analysis;
using ExpressiveAnnotations.Attributes;
using ExpressiveAnnotations.Functions;
using ExpressiveAnnotations.MvcUnobtrusive.Caching;

namespace ExpressiveAnnotations.MvcUnobtrusive.Validators
{
Expand All @@ -33,15 +35,19 @@ protected ExpressiveValidator(ModelMetadata metadata, ControllerContext context,
{
try
{
Debug.WriteLine($"[ctor entry] process: {Process.GetCurrentProcess().Id}, thread: {Thread.CurrentThread.ManagedThreadId}");

var fieldId = $"{metadata.ContainerType.FullName}.{metadata.PropertyName}".ToLowerInvariant();
AttributeFullId = $"{attribute.TypeId}.{fieldId}".ToLowerInvariant();
AttributeWeakId = $"{typeof (T).FullName}.{fieldId}".ToLowerInvariant();
FieldName = metadata.PropertyName;

ResetSuffixAllocation();

var item = MapCache.GetOrAdd(AttributeFullId, _ => // map cache is based on static dictionary, set-up once for entire application instance
var item = MapCache<string, CacheItem>.GetOrAdd(AttributeFullId, _ => // map cache is based on static dictionary, set-up once for entire application instance
{ // (by design, no reason to recompile once compiled expressions)
Debug.WriteLine($"[cache add] process: {Process.GetCurrentProcess().Id}, thread: {Thread.CurrentThread.ManagedThreadId}");

var parser = new Parser();
parser.RegisterToolchain();
parser.Parse<bool>(metadata.ContainerType, attribute.Expression);
Expand Down

0 comments on commit 8c3cd58

Please sign in to comment.