Skip to content

Commit

Permalink
Reduce response times for deployments with specialized implementations
Browse files Browse the repository at this point in the history
Reduce the time needed for completing an app deployment with some specialized implementations:

+ In case of migration, parallelize more tasks.
+ Speed up Elm App compilation with new caches. Compute a hash over the compilation arguments and store results in a dictionary. Limit memory usage of that cache by removing older items.
  • Loading branch information
Viir committed Jun 5, 2021
1 parent 700453b commit 6703459
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 59 deletions.
34 changes: 34 additions & 0 deletions implement/elm-fullstack/ElmFullstack/Cache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

namespace ElmFullstack
{
static public class Cache
{
static public void RemoveItemsToLimitRetainedSize<TKey, TValue, TOrderKey>(
IDictionary<TKey, TValue> cache,
Func<KeyValuePair<TKey, TValue>, long> computeItemSize,
Func<KeyValuePair<TKey, TValue>, TOrderKey> computeItemRetentionPriority,
long retainedSizeLimit)
{
var itemsOrderedByRetentionPrio =
cache
.OrderByDescending(computeItemRetentionPriority)
.ToImmutableList();

long aggregateSize = 0;

foreach (var item in itemsOrderedByRetentionPrio)
{
var itemSize = computeItemSize(item);

aggregateSize += itemSize;

if (retainedSizeLimit < aggregateSize)
cache.Remove(item.Key, out _);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
Expand Down Expand Up @@ -30,17 +31,62 @@ public struct ElmAppInterfaceConvention
static public IImmutableList<string> CompilationInterfaceModuleNamePrefixes => ImmutableList.Create("ElmFullstackCompilerInterface", "CompilationInterface");
}

public class ElmApp
public class ElmAppCompilation
{
static readonly System.Diagnostics.Stopwatch cacheItemTimeSource = System.Diagnostics.Stopwatch.StartNew();

static readonly ConcurrentDictionary<string, (IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> compilationResult, TimeSpan lastUseTime)> ElmAppCompilationCache =
new ConcurrentDictionary<string, (IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>>, TimeSpan)>();

static void ElmAppCompilationCacheRemoveOlderItems(long retainedSizeLimit) =>
Cache.RemoveItemsToLimitRetainedSize(
ElmAppCompilationCache,
item => 1000 + EstimateCacheItemSizeInMemory(item.Value.compilationResult),
item => item.Value.lastUseTime,
retainedSizeLimit);

static public (IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> compiledAppFiles, IImmutableList<CompilationIterationReport> iterationsReports) AsCompletelyLoweredElmApp(
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> sourceFiles,
ElmAppInterfaceConfig interfaceConfig) =>
AsCompletelyLoweredElmApp(
sourceFiles,
rootModuleName: interfaceConfig.RootModuleName.Split('.').ToImmutableList(),
interfaceToHostRootModuleName: InterfaceToHostRootModuleName.Split('.').ToImmutableList());
ElmAppInterfaceConfig interfaceConfig)
{
var sourceFilesHash =
CommonConversion.StringBase16FromByteArray(Composition.GetHash(Composition.SortedTreeFromSetOfBlobsWithStringPath(sourceFiles)));

static (IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>>, IImmutableList<CompilationIterationReport>) AsCompletelyLoweredElmApp(
var compilationHash =
CommonConversion.StringBase16FromByteArray(CommonConversion.HashSHA256(Encoding.UTF8.GetBytes(
Newtonsoft.Json.JsonConvert.SerializeObject(new
{
sourceFilesHash,
interfaceConfig,
}))));

IImmutableList<CompilationIterationReport> newCompilationIterationsReports = null;

IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> compileNew()
{
var (compiledAppFiles, iterationsReports) =
AsCompletelyLoweredElmApp(
sourceFiles,
rootModuleName: interfaceConfig.RootModuleName.Split('.').ToImmutableList(),
interfaceToHostRootModuleName: InterfaceToHostRootModuleName.Split('.').ToImmutableList());

newCompilationIterationsReports = iterationsReports;

return compiledAppFiles;
}

var (compilationResult, _) =
ElmAppCompilationCache.AddOrUpdate(
compilationHash,
_ => (compileNew(), cacheItemTimeSource.Elapsed),
(_, previousEntry) => (previousEntry.compilationResult, cacheItemTimeSource.Elapsed));

ElmAppCompilationCacheRemoveOlderItems(50_000_000);

return (compilationResult, newCompilationIterationsReports);
}

static (IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> compiledAppFiles, IImmutableList<CompilationIterationReport> iterationsReports) AsCompletelyLoweredElmApp(
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> sourceFiles,
IImmutableList<string> rootModuleName,
IImmutableList<string> interfaceToHostRootModuleName) =>
Expand All @@ -65,7 +111,7 @@ static public (IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>>
stack.SelectMany(frame => frame.discoveredDependencies)
.ToImmutableList();

var (compilationResult, compilationReport) = ElmAppCompilation(
var (compilationResult, compilationReport) = CachedElmAppCompilationIteration(
sourceFiles: sourceFiles,
rootModuleName: rootModuleName,
interfaceToHostRootModuleName: interfaceToHostRootModuleName,
Expand Down Expand Up @@ -189,7 +235,17 @@ CompilationIterationDependencyReport completeDependencyReport()
stack: stack.Push(newStackFrame));
}

static (ElmValueCommonJson.Result<IReadOnlyList<CompilerSerialInterface.CompilationError>, ImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>>>, CompilationIterationCompilationReport report) ElmAppCompilation(
static readonly ConcurrentDictionary<string, (ElmValueCommonJson.Result<IReadOnlyList<CompilerSerialInterface.CompilationError>, ImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>>> compilationResult, TimeSpan lastUseTime)> ElmAppCompilationIterationCache =
new ConcurrentDictionary<string, (ElmValueCommonJson.Result<IReadOnlyList<CompilerSerialInterface.CompilationError>, ImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>>>, TimeSpan)>();

static void ElmAppCompilationIterationCacheRemoveOlderItems(long retainedSizeLimit) =>
Cache.RemoveItemsToLimitRetainedSize(
ElmAppCompilationIterationCache,
item => 1000 + EstimateCacheItemSizeInMemory(item.Value.compilationResult),
item => item.Value.lastUseTime,
retainedSizeLimit);

static (ElmValueCommonJson.Result<IReadOnlyList<CompilerSerialInterface.CompilationError>, ImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>>>, CompilationIterationCompilationReport report) CachedElmAppCompilationIteration(
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> sourceFiles,
IImmutableList<string> rootModuleName,
IImmutableList<string> interfaceToHostRootModuleName,
Expand Down Expand Up @@ -228,42 +284,62 @@ CompilationIterationDependencyReport completeDependencyReport()
}
);

var argumentsJsonHash =
CommonConversion.StringBase16FromByteArray(CommonConversion.HashSHA256(Encoding.UTF8.GetBytes(argumentsJson)));

serializeStopwatch.Stop();

var prepareJsEngineStopwatch = System.Diagnostics.Stopwatch.StartNew();
System.Diagnostics.Stopwatch prepareJsEngineStopwatch = null;
System.Diagnostics.Stopwatch inJsEngineStopwatch = null;
System.Diagnostics.Stopwatch deserializeStopwatch = null;

ElmValueCommonJson.Result<IReadOnlyList<CompilerSerialInterface.CompilationError>, ImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>>> compileNew()
{
prepareJsEngineStopwatch = System.Diagnostics.Stopwatch.StartNew();

var jsEngine = jsEngineToCompileElmApp.Value;

prepareJsEngineStopwatch.Stop();

var jsEngine = jsEngineToCompileElmApp.Value;
inJsEngineStopwatch = System.Diagnostics.Stopwatch.StartNew();

prepareJsEngineStopwatch.Stop();
var responseJson =
jsEngine.CallFunction("lowerSerialized", argumentsJson)
?.ToString();

var inJsEngineStopwatch = System.Diagnostics.Stopwatch.StartNew();
inJsEngineStopwatch.Stop();

var responseJson =
jsEngine.CallFunction("lowerSerialized", argumentsJson)
?.ToString();
deserializeStopwatch = System.Diagnostics.Stopwatch.StartNew();

inJsEngineStopwatch.Stop();
var withFilesAsList =
Newtonsoft.Json.JsonConvert.DeserializeObject<ElmValueCommonJson.Result<IReadOnlyList<CompilerSerialInterface.CompilationError>, IReadOnlyList<CompilerSerialInterface.AppCodeEntry>>>(responseJson);

var deserializeStopwatch = System.Diagnostics.Stopwatch.StartNew();
var mappedResult =
withFilesAsList.map(files =>
files.ToImmutableDictionary(
entry => (IImmutableList<string>)entry.path.ToImmutableList(),
entry => (IReadOnlyList<byte>)Convert.FromBase64String(entry.content.AsBase64))
.WithComparers(EnumerableExtension.EqualityComparer<string>()));

var withFilesAsList =
Newtonsoft.Json.JsonConvert.DeserializeObject<ElmValueCommonJson.Result<IReadOnlyList<CompilerSerialInterface.CompilationError>, IReadOnlyList<CompilerSerialInterface.AppCodeEntry>>>(responseJson);
return mappedResult;
}

var result =
ElmAppCompilationIterationCache.AddOrUpdate(
argumentsJsonHash,
_ => (compileNew(), cacheItemTimeSource.Elapsed),
(_, previousEntry) => (previousEntry.compilationResult, cacheItemTimeSource.Elapsed));

var mappedResult =
withFilesAsList.map(files =>
files.ToImmutableDictionary(
entry => (IImmutableList<string>)entry.path.ToImmutableList(),
entry => (IReadOnlyList<byte>)Convert.FromBase64String(entry.content.AsBase64))
.WithComparers(EnumerableExtension.EqualityComparer<string>()));
ElmAppCompilationIterationCacheRemoveOlderItems(50_000_000);

return
(mappedResult,
(result.compilationResult,
new CompilationIterationCompilationReport
{
serializeTimeSpentMilli = (int)serializeStopwatch.ElapsedMilliseconds,
prepareJsEngineTimeSpentMilli = (int)prepareJsEngineStopwatch.ElapsedMilliseconds,
inJsEngineTimeSpentMilli = (int)inJsEngineStopwatch.ElapsedMilliseconds,
deserializeTimeSpentMilli = (int)deserializeStopwatch.ElapsedMilliseconds,
prepareJsEngineTimeSpentMilli = (int?)prepareJsEngineStopwatch?.ElapsedMilliseconds,
inJsEngineTimeSpentMilli = (int?)inJsEngineStopwatch?.ElapsedMilliseconds,
deserializeTimeSpentMilli = (int?)deserializeStopwatch?.ElapsedMilliseconds,
totalTimeSpentMilli = (int)totalStopwatch.ElapsedMilliseconds,
});
}
Expand All @@ -281,7 +357,7 @@ static public IImmutableList<string> FilePathFromModuleName(string moduleName) =

static public string InterfaceToHostRootModuleName => "Backend.InterfaceToHost_Root";

static Lazy<JavaScriptEngineSwitcher.Core.IJsEngine> jsEngineToCompileElmApp = new Lazy<JavaScriptEngineSwitcher.Core.IJsEngine>(PrepareJsEngineToCompileElmApp);
static readonly Lazy<JavaScriptEngineSwitcher.Core.IJsEngine> jsEngineToCompileElmApp = new Lazy<JavaScriptEngineSwitcher.Core.IJsEngine>(PrepareJsEngineToCompileElmApp);

static public JavaScriptEngineSwitcher.Core.IJsEngine PrepareJsEngineToCompileElmApp()
{
Expand Down Expand Up @@ -328,7 +404,7 @@ static public IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>>

static byte[] GetManifestResourceStreamContent(string name)
{
using var stream = typeof(ElmApp).Assembly.GetManifestResourceStream(name);
using var stream = typeof(ElmAppCompilation).Assembly.GetManifestResourceStream(name);
using var memoryStream = new System.IO.MemoryStream();

stream.CopyTo(memoryStream);
Expand All @@ -339,6 +415,23 @@ static byte[] GetManifestResourceStreamContent(string name)
static string DescribeCompilationError(CompilerSerialInterface.CompilationError compilationError) =>
Newtonsoft.Json.JsonConvert.SerializeObject(compilationError, Newtonsoft.Json.Formatting.Indented);


static long EstimateCacheItemSizeInMemory(ElmValueCommonJson.Result<IReadOnlyList<CompilerSerialInterface.CompilationError>, ImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>>> item) =>
(item.Err?.Sum(err => err.Sum(EstimateCacheItemSizeInMemory)) ?? 0) +
(item.Ok?.Sum(EstimateCacheItemSizeInMemory) ?? 0);

static long EstimateCacheItemSizeInMemory(IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> item) =>
(item?.Sum(file => file.Key.Sum(e => e.Length) + file.Value.Count)) ?? 0;

static long EstimateCacheItemSizeInMemory(CompilerSerialInterface.CompilationError compilationError) =>
compilationError?.MissingDependencyError?.Sum(EstimateCacheItemSizeInMemory) ?? 0;

static long EstimateCacheItemSizeInMemory(CompilerSerialInterface.DependencyKey dependencyKey) =>
dependencyKey.ElmMakeDependency?.Sum(EstimateCacheItemSizeInMemory) ?? 0;

static long EstimateCacheItemSizeInMemory(CompilerSerialInterface.ElmMakeRequestStructure elmMakeRequest) =>
elmMakeRequest?.files?.Sum(file => file.content.AsBase64.Length) ?? 0;

public class CompilationIterationReport
{
public CompilationIterationCompilationReport compilation;
Expand All @@ -352,11 +445,11 @@ public class CompilationIterationCompilationReport
{
public int serializeTimeSpentMilli;

public int prepareJsEngineTimeSpentMilli;
public int? prepareJsEngineTimeSpentMilli;

public int inJsEngineTimeSpentMilli;
public int? inJsEngineTimeSpentMilli;

public int deserializeTimeSpentMilli;
public int? deserializeTimeSpentMilli;

public int totalTimeSpentMilli;
}
Expand Down Expand Up @@ -385,7 +478,7 @@ struct DependencyKey

class ElmMakeRequestStructure
{
public IReadOnlyList<CompilerSerialInterface.AppCodeEntry> files;
public IReadOnlyList<AppCodeEntry> files;

public IReadOnlyList<string> entryPointFilePath;

Expand Down
4 changes: 2 additions & 2 deletions implement/elm-fullstack/ElmFullstack/Process.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ static public (IDisposableProcessWithStringInterface process,

var javascriptFromElmMake = CompileElmToJavascript(
elmCodeFiles,
ElmApp.FilePathFromModuleName(ElmApp.InterfaceToHostRootModuleName));
ElmAppCompilation.FilePathFromModuleName(ElmAppCompilation.InterfaceToHostRootModuleName));

var pathToFunctionCommonStart = ElmApp.InterfaceToHostRootModuleName + ".";
var pathToFunctionCommonStart = ElmAppCompilation.InterfaceToHostRootModuleName + ".";

var javascriptPreparedToRun =
BuildAppJavascript(
Expand Down
8 changes: 4 additions & 4 deletions implement/elm-fullstack/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace elm_fullstack
{
public class Program
{
static public string AppVersionId => "2021-06-04";
static public string AppVersionId => "2021-06-05";

static int AdminInterfaceDefaultPort => 4000;

Expand Down Expand Up @@ -556,11 +556,11 @@ static CommandLineApplication AddCompileAppCmd(CommandLineApplication app) =>

string compilationException = null;
Composition.TreeWithStringPath compiledTree = null;
IImmutableList<ElmFullstack.ElmApp.CompilationIterationReport> compilationIterationsReports = null;
IImmutableList<ElmFullstack.ElmAppCompilation.CompilationIterationReport> compilationIterationsReports = null;

try
{
var (compiledAppFiles, iterationsReports) = ElmFullstack.ElmApp.AsCompletelyLoweredElmApp(
var (compiledAppFiles, iterationsReports) = ElmFullstack.ElmAppCompilation.AsCompletelyLoweredElmApp(
sourceFiles: sourceFiles,
ElmFullstack.ElmAppInterfaceConfig.Default);

Expand Down Expand Up @@ -816,7 +816,7 @@ public class CompileAppReport

public SourceSummaryStructure sourceSummary;

public IImmutableList<ElmFullstack.ElmApp.CompilationIterationReport> compilationIterationsReports;
public IImmutableList<ElmFullstack.ElmAppCompilation.CompilationIterationReport> compilationIterationsReports;

public string compilationException;

Expand Down
Loading

0 comments on commit 6703459

Please sign in to comment.