diff --git a/implement/elm-fullstack/ElmFullstack/Cache.cs b/implement/elm-fullstack/ElmFullstack/Cache.cs new file mode 100644 index 00000000..9aa2f684 --- /dev/null +++ b/implement/elm-fullstack/ElmFullstack/Cache.cs @@ -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( + IDictionary cache, + Func, long> computeItemSize, + Func, 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 _); + } + } + } +} diff --git a/implement/elm-fullstack/ElmFullstack/ElmApp.cs b/implement/elm-fullstack/ElmFullstack/ElmAppCompilation.cs similarity index 66% rename from implement/elm-fullstack/ElmFullstack/ElmApp.cs rename to implement/elm-fullstack/ElmFullstack/ElmAppCompilation.cs index 11d9dd7c..1d267d60 100644 --- a/implement/elm-fullstack/ElmFullstack/ElmApp.cs +++ b/implement/elm-fullstack/ElmFullstack/ElmAppCompilation.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -30,17 +31,62 @@ public struct ElmAppInterfaceConvention static public IImmutableList CompilationInterfaceModuleNamePrefixes => ImmutableList.Create("ElmFullstackCompilerInterface", "CompilationInterface"); } - public class ElmApp + public class ElmAppCompilation { + static readonly System.Diagnostics.Stopwatch cacheItemTimeSource = System.Diagnostics.Stopwatch.StartNew(); + + static readonly ConcurrentDictionary, IReadOnlyList> compilationResult, TimeSpan lastUseTime)> ElmAppCompilationCache = + new ConcurrentDictionary, IReadOnlyList>, TimeSpan)>(); + + static void ElmAppCompilationCacheRemoveOlderItems(long retainedSizeLimit) => + Cache.RemoveItemsToLimitRetainedSize( + ElmAppCompilationCache, + item => 1000 + EstimateCacheItemSizeInMemory(item.Value.compilationResult), + item => item.Value.lastUseTime, + retainedSizeLimit); + static public (IImmutableDictionary, IReadOnlyList> compiledAppFiles, IImmutableList iterationsReports) AsCompletelyLoweredElmApp( IImmutableDictionary, IReadOnlyList> 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, IReadOnlyList>, IImmutableList) AsCompletelyLoweredElmApp( + var compilationHash = + CommonConversion.StringBase16FromByteArray(CommonConversion.HashSHA256(Encoding.UTF8.GetBytes( + Newtonsoft.Json.JsonConvert.SerializeObject(new + { + sourceFilesHash, + interfaceConfig, + })))); + + IImmutableList newCompilationIterationsReports = null; + + IImmutableDictionary, IReadOnlyList> 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, IReadOnlyList> compiledAppFiles, IImmutableList iterationsReports) AsCompletelyLoweredElmApp( IImmutableDictionary, IReadOnlyList> sourceFiles, IImmutableList rootModuleName, IImmutableList interfaceToHostRootModuleName) => @@ -65,7 +111,7 @@ static public (IImmutableDictionary, IReadOnlyList> stack.SelectMany(frame => frame.discoveredDependencies) .ToImmutableList(); - var (compilationResult, compilationReport) = ElmAppCompilation( + var (compilationResult, compilationReport) = CachedElmAppCompilationIteration( sourceFiles: sourceFiles, rootModuleName: rootModuleName, interfaceToHostRootModuleName: interfaceToHostRootModuleName, @@ -189,7 +235,17 @@ CompilationIterationDependencyReport completeDependencyReport() stack: stack.Push(newStackFrame)); } - static (ElmValueCommonJson.Result, ImmutableDictionary, IReadOnlyList>>, CompilationIterationCompilationReport report) ElmAppCompilation( + static readonly ConcurrentDictionary, ImmutableDictionary, IReadOnlyList>> compilationResult, TimeSpan lastUseTime)> ElmAppCompilationIterationCache = + new ConcurrentDictionary, ImmutableDictionary, IReadOnlyList>>, TimeSpan)>(); + + static void ElmAppCompilationIterationCacheRemoveOlderItems(long retainedSizeLimit) => + Cache.RemoveItemsToLimitRetainedSize( + ElmAppCompilationIterationCache, + item => 1000 + EstimateCacheItemSizeInMemory(item.Value.compilationResult), + item => item.Value.lastUseTime, + retainedSizeLimit); + + static (ElmValueCommonJson.Result, ImmutableDictionary, IReadOnlyList>>, CompilationIterationCompilationReport report) CachedElmAppCompilationIteration( IImmutableDictionary, IReadOnlyList> sourceFiles, IImmutableList rootModuleName, IImmutableList interfaceToHostRootModuleName, @@ -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, ImmutableDictionary, IReadOnlyList>> 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, IReadOnlyList>>(responseJson); - var deserializeStopwatch = System.Diagnostics.Stopwatch.StartNew(); + var mappedResult = + withFilesAsList.map(files => + files.ToImmutableDictionary( + entry => (IImmutableList)entry.path.ToImmutableList(), + entry => (IReadOnlyList)Convert.FromBase64String(entry.content.AsBase64)) + .WithComparers(EnumerableExtension.EqualityComparer())); - var withFilesAsList = - Newtonsoft.Json.JsonConvert.DeserializeObject, IReadOnlyList>>(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)entry.path.ToImmutableList(), - entry => (IReadOnlyList)Convert.FromBase64String(entry.content.AsBase64)) - .WithComparers(EnumerableExtension.EqualityComparer())); + 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, }); } @@ -281,7 +357,7 @@ static public IImmutableList FilePathFromModuleName(string moduleName) = static public string InterfaceToHostRootModuleName => "Backend.InterfaceToHost_Root"; - static Lazy jsEngineToCompileElmApp = new Lazy(PrepareJsEngineToCompileElmApp); + static readonly Lazy jsEngineToCompileElmApp = new Lazy(PrepareJsEngineToCompileElmApp); static public JavaScriptEngineSwitcher.Core.IJsEngine PrepareJsEngineToCompileElmApp() { @@ -328,7 +404,7 @@ static public IImmutableDictionary, IReadOnlyList> 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); @@ -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, ImmutableDictionary, IReadOnlyList>> item) => + (item.Err?.Sum(err => err.Sum(EstimateCacheItemSizeInMemory)) ?? 0) + + (item.Ok?.Sum(EstimateCacheItemSizeInMemory) ?? 0); + + static long EstimateCacheItemSizeInMemory(IImmutableDictionary, IReadOnlyList> 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; @@ -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; } @@ -385,7 +478,7 @@ struct DependencyKey class ElmMakeRequestStructure { - public IReadOnlyList files; + public IReadOnlyList files; public IReadOnlyList entryPointFilePath; diff --git a/implement/elm-fullstack/ElmFullstack/Process.cs b/implement/elm-fullstack/ElmFullstack/Process.cs index b1ef7fcd..65f74232 100644 --- a/implement/elm-fullstack/ElmFullstack/Process.cs +++ b/implement/elm-fullstack/ElmFullstack/Process.cs @@ -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( diff --git a/implement/elm-fullstack/Program.cs b/implement/elm-fullstack/Program.cs index a8e717c3..c5fe6d9f 100644 --- a/implement/elm-fullstack/Program.cs +++ b/implement/elm-fullstack/Program.cs @@ -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; @@ -556,11 +556,11 @@ static CommandLineApplication AddCompileAppCmd(CommandLineApplication app) => string compilationException = null; Composition.TreeWithStringPath compiledTree = null; - IImmutableList compilationIterationsReports = null; + IImmutableList compilationIterationsReports = null; try { - var (compiledAppFiles, iterationsReports) = ElmFullstack.ElmApp.AsCompletelyLoweredElmApp( + var (compiledAppFiles, iterationsReports) = ElmFullstack.ElmAppCompilation.AsCompletelyLoweredElmApp( sourceFiles: sourceFiles, ElmFullstack.ElmAppInterfaceConfig.Default); @@ -816,7 +816,7 @@ public class CompileAppReport public SourceSummaryStructure sourceSummary; - public IImmutableList compilationIterationsReports; + public IImmutableList compilationIterationsReports; public string compilationException; diff --git a/implement/elm-fullstack/WebHost/PersistentProcessVolatileRepresentation.cs b/implement/elm-fullstack/WebHost/PersistentProcessVolatileRepresentation.cs index cbd75dc2..6650145f 100644 --- a/implement/elm-fullstack/WebHost/PersistentProcessVolatileRepresentation.cs +++ b/implement/elm-fullstack/WebHost/PersistentProcessVolatileRepresentation.cs @@ -88,7 +88,7 @@ static public (IDisposableProcessWithStringInterface process, { var sourceFiles = Composition.TreeToFlatDictionaryWithPathComparer(appConfig); - var (loweredAppFiles, _) = ElmApp.AsCompletelyLoweredElmApp( + var (loweredAppFiles, _) = ElmAppCompilation.AsCompletelyLoweredElmApp( sourceFiles: sourceFiles, ElmAppInterfaceConfig.Default); @@ -397,13 +397,17 @@ static PersistentProcessVolatileRepresentationDuringRestore ApplyCompositionEven var appConfig = compositionEvent.DeployAppConfigAndMigrateElmAppState; - var prepareMigrateResult = - PrepareMigrateSerializedValue(destinationAppConfigTree: appConfig); + var prepareMigrateTask = System.Threading.Tasks.Task.Run(() => + PrepareMigrateSerializedValue(destinationAppConfigTree: appConfig)); - var (newElmAppProcess, buildArtifacts) = + var processFromWebAppConfigTask = System.Threading.Tasks.Task.Run(() => ProcessFromWebAppConfig( appConfig, - overrideElmAppInterfaceConfig: overrideElmAppInterfaceConfig); + overrideElmAppInterfaceConfig: overrideElmAppInterfaceConfig)); + + var prepareMigrateResult = prepareMigrateTask.Result; + + var (newElmAppProcess, buildArtifacts) = processFromWebAppConfigTask.Result; string migratedSerializedState = null; Result setElmAppStateResult = null; @@ -573,11 +577,11 @@ static Result>> PrepareMigrateSerial var appConfigFiles = Composition.TreeToFlatDictionaryWithPathComparer(destinationAppConfigTree); - var pathToInterfaceModuleFile = ElmApp.FilePathFromModuleName(MigrationElmAppInterfaceModuleName); - var pathToCompilationRootModuleFile = ElmApp.FilePathFromModuleName(MigrationElmAppCompilationRootModuleName); + var pathToInterfaceModuleFile = ElmAppCompilation.FilePathFromModuleName(MigrationElmAppInterfaceModuleName); + var pathToCompilationRootModuleFile = ElmAppCompilation.FilePathFromModuleName(MigrationElmAppCompilationRootModuleName); if (!appConfigFiles.TryGetValue(pathToInterfaceModuleFile, out var _) && - !appConfigFiles.TryGetValue(ElmApp.FilePathFromModuleName(Old_MigrationElmAppInterfaceModuleName), out var _)) + !appConfigFiles.TryGetValue(ElmAppCompilation.FilePathFromModuleName(Old_MigrationElmAppInterfaceModuleName), out var _)) return new Result>> { Err = "Did not find interface module at '" + string.Join("/", pathToInterfaceModuleFile) + "'", @@ -585,18 +589,18 @@ static Result>> PrepareMigrateSerial try { - var (migrateElmAppFiles, _) = ElmApp.AsCompletelyLoweredElmApp( + var (migrateElmAppFiles, _) = ElmAppCompilation.AsCompletelyLoweredElmApp( sourceFiles: appConfigFiles, interfaceConfig: ElmAppInterfaceConfig.Default); - var javascriptFromElmMake = ElmFullstack.ProcessFromElm019Code.CompileElmToJavascript( + var javascriptFromElmMake = ProcessFromElm019Code.CompileElmToJavascript( migrateElmAppFiles, pathToCompilationRootModuleFile); - var javascriptMinusCrashes = ElmFullstack.ProcessFromElm019Code.JavascriptMinusCrashes(javascriptFromElmMake); + var javascriptMinusCrashes = ProcessFromElm019Code.JavascriptMinusCrashes(javascriptFromElmMake); var javascriptToRun = - ElmFullstack.ProcessFromElm019Code.PublishFunctionsFromJavascriptFromElmMake( + ProcessFromElm019Code.PublishFunctionsFromJavascriptFromElmMake( javascriptMinusCrashes, new[] { @@ -620,7 +624,7 @@ static Result>> PrepareMigrateSerial } var migrateResultStructure = - JsonConvert.DeserializeObject>(migrateResultString); + JsonConvert.DeserializeObject>(migrateResultString); var elmAppStateMigratedSerialized = migrateResultStructure?.Ok?.FirstOrDefault(); diff --git a/implement/elm-fullstack/elm-fullstack.csproj b/implement/elm-fullstack/elm-fullstack.csproj index f6eca078..ac24d3a7 100644 --- a/implement/elm-fullstack/elm-fullstack.csproj +++ b/implement/elm-fullstack/elm-fullstack.csproj @@ -5,8 +5,8 @@ netcoreapp3.1 elm_fullstack elm-fs - 2021.0604.0.0 - 2021.0604.0.0 + 2021.0605.0.0 + 2021.0605.0.0 diff --git a/implement/test-elm-fullstack/TestModeledInElm.cs b/implement/test-elm-fullstack/TestModeledInElm.cs index f858fe65..64d38e7b 100644 --- a/implement/test-elm-fullstack/TestModeledInElm.cs +++ b/implement/test-elm-fullstack/TestModeledInElm.cs @@ -22,7 +22,7 @@ static IImmutableDictionary, IReadOnlyList> GetLowe IImmutableList directoryPath) { return - ElmApp.AsCompletelyLoweredElmApp( + ElmAppCompilation.AsCompletelyLoweredElmApp( sourceFiles: TestSetup.GetElmAppFromDirectoryPath(directoryPath), ElmAppInterfaceConfig.Default).compiledAppFiles; } diff --git a/implement/test-elm-fullstack/TestSetup.cs b/implement/test-elm-fullstack/TestSetup.cs index 4a670231..f7318215 100644 --- a/implement/test-elm-fullstack/TestSetup.cs +++ b/implement/test-elm-fullstack/TestSetup.cs @@ -95,7 +95,7 @@ static public IImmutableDictionary, IReadOnlyList> static public IImmutableDictionary, IReadOnlyList> AsLoweredElmApp( IImmutableDictionary, IReadOnlyList> originalAppFiles) => - ElmApp.AsCompletelyLoweredElmApp( + ElmAppCompilation.AsCompletelyLoweredElmApp( sourceFiles: originalAppFiles, ElmAppInterfaceConfig.Default).compiledAppFiles;