Skip to content

Commit

Permalink
Speed up compilation for Elm Interactive by caching
Browse files Browse the repository at this point in the history
Also, improve Pine blob representations by switching to `ReadOnlyMemory<byte>`
  • Loading branch information
Viir committed Jul 2, 2022
1 parent ead647b commit 89f8847
Show file tree
Hide file tree
Showing 28 changed files with 547 additions and 506 deletions.
56 changes: 28 additions & 28 deletions implement/elm-fullstack/ElmFullstack/ElmAppCompilation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ static void ElmAppCompilationCacheRemoveOlderItems(long retainedSizeLimit) =>
retainedSizeLimit);

static public Result<IReadOnlyList<LocatedCompilationError>, CompilationSuccess> AsCompletelyLoweredElmApp(
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> sourceFiles,
IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>> sourceFiles,
ElmAppInterfaceConfig interfaceConfig)
{
var sourceFilesHash =
Expand Down Expand Up @@ -84,15 +84,15 @@ Result<IReadOnlyList<LocatedCompilationError>, CompilationSuccess> compileNew()
}

public record CompilationSuccess(
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> compiledAppFiles,
IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>> compiledAppFiles,
IImmutableList<CompilationIterationReport> iterationsReports);

public record StackFrame(
IImmutableList<(CompilerSerialInterface.DependencyKey key, IReadOnlyList<byte> value)> discoveredDependencies,
IImmutableList<(CompilerSerialInterface.DependencyKey key, ReadOnlyMemory<byte> value)> discoveredDependencies,
CompilationIterationReport iterationReport);

static Result<IReadOnlyList<LocatedCompilationError>, CompilationSuccess> AsCompletelyLoweredElmApp(
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> sourceFiles,
IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>> sourceFiles,
IImmutableList<string> rootModuleName,
IImmutableList<string> interfaceToHostRootModuleName) =>
AsCompletelyLoweredElmApp(
Expand All @@ -102,7 +102,7 @@ static Result<IReadOnlyList<LocatedCompilationError>, CompilationSuccess> AsComp
ImmutableStack<StackFrame>.Empty);

static Result<IReadOnlyList<LocatedCompilationError>, CompilationSuccess> AsCompletelyLoweredElmApp(
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> sourceFiles,
IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>> sourceFiles,
IImmutableList<string> rootModuleName,
IImmutableList<string> interfaceToHostRootModuleName,
IImmutableStack<StackFrame> stack)
Expand Down Expand Up @@ -169,7 +169,7 @@ static Result<IReadOnlyList<LocatedCompilationError>, CompilationSuccess> AsComp
}

byte[] ElmMake(
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> elmCodeFiles,
IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>> elmCodeFiles,
IImmutableList<string> pathToFileWithElmEntryPoint,
bool makeJavascript,
bool enableDebug)
Expand Down Expand Up @@ -201,15 +201,15 @@ TimedReport<CompilationIterationDependencyReport> completeDependencyReport(Compi

if (elmMakeRequest != null)
{
Result<string, IReadOnlyList<byte>> buildResultValue()
Result<string, ReadOnlyMemory<byte>?> buildResultValue()
{
try
{
var elmMakeRequestFiles =
elmMakeRequest.files
.ToImmutableDictionary(
entry => (IImmutableList<string>)entry.path.ToImmutableList(),
entry => (IReadOnlyList<byte>)Convert.FromBase64String(entry.content.AsBase64))
entry => (ReadOnlyMemory<byte>)Convert.FromBase64String(entry.content.AsBase64))
.WithComparers(EnumerableExtension.EqualityComparer<IImmutableList<string>>());

var value = ElmMake(
Expand All @@ -218,11 +218,11 @@ Result<string, IReadOnlyList<byte>> buildResultValue()
makeJavascript: elmMakeRequest.outputType.ElmMakeOutputTypeJs != null,
enableDebug: elmMakeRequest.enableDebug);

return Result<string, IReadOnlyList<byte>>.ok(value);
return Result<string, ReadOnlyMemory<byte>?>.ok(value);
}
catch (Exception e)
{
return Result<string, IReadOnlyList<byte>>.err("Failed with runtime exception: " + e.ToString());
return Result<string, ReadOnlyMemory<byte>?>.err("Failed with runtime exception: " + e.ToString());
}
}

Expand Down Expand Up @@ -260,7 +260,7 @@ currentIterationReport with

var newStackFrame =
new StackFrame(
newDependencies.Select(depAndReport => (depAndReport.Item1.key, depAndReport.Item1.result.Ok!)).ToImmutableList(),
newDependencies.Select(depAndReport => (depAndReport.Item1.key, depAndReport.Item1.result.Ok!.Value)).ToImmutableList(),
currentIterationReport);

return AsCompletelyLoweredElmApp(
Expand All @@ -270,7 +270,7 @@ currentIterationReport with
stack: stack.Push(newStackFrame));
}

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

static void ElmAppCompilationIterationCacheRemoveOlderItems(long retainedSizeLimit) =>
Cache.RemoveItemsToLimitRetainedSize(
Expand All @@ -279,12 +279,12 @@ static void ElmAppCompilationIterationCacheRemoveOlderItems(long retainedSizeLim
item => item.Value.lastUseTime,
retainedSizeLimit);

static (ElmValueCommonJson.Result<IReadOnlyList<CompilerSerialInterface.LocatedCompilationError>, ImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>>>, CompilationIterationCompilationReport report) CachedElmAppCompilationIteration(
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> compilerElmProgramCodeFiles,
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> sourceFiles,
static (ElmValueCommonJson.Result<IReadOnlyList<CompilerSerialInterface.LocatedCompilationError>, ImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>>>, CompilationIterationCompilationReport report) CachedElmAppCompilationIteration(
IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>> compilerElmProgramCodeFiles,
IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>> sourceFiles,
IImmutableList<string> rootModuleName,
IImmutableList<string> interfaceToHostRootModuleName,
ImmutableList<(CompilerSerialInterface.DependencyKey key, IReadOnlyList<byte> value)> dependencies)
ImmutableList<(CompilerSerialInterface.DependencyKey key, ReadOnlyMemory<byte> value)> dependencies)
{
var totalStopwatch = System.Diagnostics.Stopwatch.StartNew();
var serializeStopwatch = System.Diagnostics.Stopwatch.StartNew();
Expand Down Expand Up @@ -328,7 +328,7 @@ static void ElmAppCompilationIterationCacheRemoveOlderItems(long retainedSizeLim
System.Diagnostics.Stopwatch? inJsEngineStopwatch = null;
System.Diagnostics.Stopwatch? deserializeStopwatch = null;

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

Expand Down Expand Up @@ -363,7 +363,7 @@ static void ElmAppCompilationIterationCacheRemoveOlderItems(long retainedSizeLim
compilationResponseOk.map(files =>
files.ToImmutableDictionary(
entry => (IImmutableList<string>)entry.path.ToImmutableList(),
entry => (IReadOnlyList<byte>)Convert.FromBase64String(entry.content.AsBase64))
entry => (ReadOnlyMemory<byte>)Convert.FromBase64String(entry.content.AsBase64))
.WithComparers(EnumerableExtension.EqualityComparer<IImmutableList<string>>()));

return mappedResult;
Expand Down Expand Up @@ -403,7 +403,7 @@ static public IImmutableList<string> FilePathFromModuleName(string moduleName) =
static public string InterfaceToHostRootModuleName => "Backend.InterfaceToHost_Root";

static public JavaScriptEngineSwitcher.Core.IJsEngine CachedJsEngineToCompileFileTree(
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> compilerElmProgramCodeFiles)
IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>> compilerElmProgramCodeFiles)
{
var compilerId =
CommonConversion.StringBase16FromByteArray(
Expand All @@ -419,7 +419,7 @@ static public JavaScriptEngineSwitcher.Core.IJsEngine CachedJsEngineToCompileFil
static readonly ConcurrentDictionary<string, JavaScriptEngineSwitcher.Core.IJsEngine> FileTreeCompilerJsEngineCache = new();

static public JavaScriptEngineSwitcher.Core.IJsEngine CreateJsEngineToCompileFileTree(
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> compilerElmProgramCodeFiles)
IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>> compilerElmProgramCodeFiles)
{
var javascript = BuildJavascriptToCompileFileTree(compilerElmProgramCodeFiles);

Expand All @@ -430,7 +430,7 @@ static public JavaScriptEngineSwitcher.Core.IJsEngine CreateJsEngineToCompileFil
return javascriptEngine;
}

static string BuildJavascriptToCompileFileTree(IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> compilerElmProgramCodeFiles)
static string BuildJavascriptToCompileFileTree(IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>> compilerElmProgramCodeFiles)
{
var javascriptFromElmMake =
ProcessFromElm019Code.CompileElmToJavascript(
Expand All @@ -453,10 +453,10 @@ static string BuildJavascriptToCompileFileTree(IImmutableDictionary<IImmutableLi
listFunctionToPublish);
}

static readonly public Lazy<Result<string, IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>>>> CachedCompilerElmProgramCodeFilesForElmFullstackBackend =
static readonly public Lazy<Result<string, IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>>>> CachedCompilerElmProgramCodeFilesForElmFullstackBackend =
new(LoadCompilerElmProgramCodeFilesForElmFullstackBackend);

static public Result<string, IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>>> LoadCompilerElmProgramCodeFilesForElmFullstackBackend() =>
static public Result<string, IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>>> LoadCompilerElmProgramCodeFilesForElmFullstackBackend() =>
DotNetAssembly.LoadFromAssemblyManifestResourceStreamContents(
filePaths:
new[]
Expand Down Expand Up @@ -548,7 +548,7 @@ static string DescribeCompilationError(CompilerSerialInterface.CompilationError
});
}

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

Expand All @@ -565,8 +565,8 @@ static long EstimateCacheItemSizeInMemory(Result<IReadOnlyList<LocatedCompilatio
static long EstimateCacheItemSizeInMemory(CompilationSuccess compilationSuccess) =>
EstimateCacheItemSizeInMemory(compilationSuccess.compiledAppFiles);

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(IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>> item) =>
(item?.Sum(file => file.Key.Sum(e => e.Length) + file.Value.Length)) ?? 0;

static long EstimateCacheItemSizeInMemory(LocatedCompilationError compilationError) =>
100 + (compilationError.location?.filePath.Sum(e => e.Length) ?? 0) + EstimateCacheItemSizeInMemory(compilationError.error);
Expand Down Expand Up @@ -642,8 +642,8 @@ public record AppCodeEntry(IReadOnlyList<string> path, BytesJson content);

public record BytesJson(string AsBase64)
{
static public BytesJson AsJson(IReadOnlyList<byte> bytes) =>
new(AsBase64: Convert.ToBase64String(bytes as byte[] ?? bytes.ToArray()));
static public BytesJson AsJson(ReadOnlyMemory<byte> bytes) =>
new(AsBase64: Convert.ToBase64String(bytes.Span));
}
}
}
2 changes: 1 addition & 1 deletion implement/elm-fullstack/ElmFullstack/PersistentProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class PersistentProcessWithHistoryOnFileFromElm019Code : IPersistentProce

public PersistentProcessWithHistoryOnFileFromElm019Code(
IProcessStoreReader storeReader,
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> elmAppFiles,
IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>> elmAppFiles,
Action<string> logger,
ElmAppInterfaceConfig? overrideElmAppInterfaceConfig = null)
{
Expand Down
27 changes: 15 additions & 12 deletions implement/elm-fullstack/ElmFullstack/Process.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,14 @@ public class ProcessFromElm019Code
static public (IDisposableProcessWithStringInterface process,
(string javascriptFromElmMake, string javascriptPreparedToRun) buildArtifacts)
ProcessFromElmCodeFiles(
IReadOnlyCollection<(IImmutableList<string>, IReadOnlyList<byte>)> elmCodeFiles,
IReadOnlyCollection<(IImmutableList<string>, ReadOnlyMemory<byte>)> elmCodeFiles,
ElmAppInterfaceConfig? overrideElmAppInterfaceConfig = null) =>
ProcessFromElmCodeFiles(Composition.ToFlatDictionaryWithPathComparer(elmCodeFiles), overrideElmAppInterfaceConfig);

static public (IDisposableProcessWithStringInterface process,
(string javascriptFromElmMake, string javascriptPreparedToRun) buildArtifacts)
ProcessFromElmCodeFiles(
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> elmCodeFiles,
IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>> elmCodeFiles,
ElmAppInterfaceConfig? overrideElmAppInterfaceConfig = null)
{
var elmAppInterfaceConfig = overrideElmAppInterfaceConfig ?? ElmAppInterfaceConfig.Default;
Expand All @@ -147,13 +147,13 @@ static public (IDisposableProcessWithStringInterface process,
}

static public string CompileElmToJavascript(
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> elmCodeFiles,
IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>> elmCodeFiles,
IImmutableList<string> pathToFileWithElmEntryPoint,
string? elmMakeCommandAppendix = null) =>
CompileElm(elmCodeFiles, pathToFileWithElmEntryPoint, "file-for-elm-make-output.js", elmMakeCommandAppendix);

static public string CompileElmToHtml(
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> elmCodeFiles,
IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>> elmCodeFiles,
IImmutableList<string> pathToFileWithElmEntryPoint,
string? elmMakeCommandAppendix = null) =>
CompileElm(elmCodeFiles, pathToFileWithElmEntryPoint, "file-for-elm-make-output.html", elmMakeCommandAppendix);
Expand All @@ -179,7 +179,7 @@ I cannot find that directory though! Is it missing? Is there a typo?
[...]
*/
static public string CompileElm(
IImmutableDictionary<IImmutableList<string>, IReadOnlyList<byte>> elmCodeFiles,
IImmutableDictionary<IImmutableList<string>, ReadOnlyMemory<byte>> elmCodeFiles,
IImmutableList<string> pathToFileWithElmEntryPoint,
string outputFileName,
string? elmMakeCommandAppendix = null)
Expand All @@ -193,7 +193,7 @@ static public string CompileElm(

var command = "make " + Filesystem.MakePlatformSpecificPath(pathToFileWithElmEntryPoint) + " --output=\"" + outputFileName + "\" " + elmMakeCommandAppendix;

var attemptsResults = new List<(ExecutableFile.ProcessOutput processOutput, IReadOnlyCollection<(IImmutableList<string> path, IReadOnlyList<byte> content)> resultingFiles)>();
var attemptsResults = new List<(ExecutableFile.ProcessOutput processOutput, IReadOnlyCollection<(IImmutableList<string> path, ReadOnlyMemory<byte> content)> resultingFiles)>();

do
{
Expand Down Expand Up @@ -237,12 +237,15 @@ An alternative would be retrying when this error is parsed from `commandResults.
.Where(file => !elmCodeFiles.ContainsKey(file.path))
.ToImmutableList();

var outputFileContent =
var outputFiles =
newFiles
.FirstOrDefault(resultFile => resultFile.path.SequenceEqual(ImmutableList.Create(outputFileName))).content;
.Where(resultFile => resultFile.path.SequenceEqual(ImmutableList.Create(outputFileName)))
.ToImmutableList();

if (outputFileContent != null)
return Encoding.UTF8.GetString(outputFileContent.ToArray());
if (1 <= outputFiles.Count)
{
return Encoding.UTF8.GetString(outputFiles.First().content.Span);
}

var errorQualifiesForRetry =
commandResults.processOutput.StandardError?.Contains("openBinaryFile: resource busy (file is locked)") ?? false;
Expand All @@ -261,8 +264,8 @@ An alternative would be retrying when this error is parsed from `commandResults.
"\nStandard Error:\n'" + lastAttemptResults.processOutput.StandardError + "'");
}

static public byte[] GetElmExecutableFile =>
BlobLibrary.LoadFileForCurrentOs(ElmExecutableFileByOs)!;
static public ReadOnlyMemory<byte> GetElmExecutableFile =>
BlobLibrary.LoadFileForCurrentOs(ElmExecutableFileByOs)!.Value;

static public IReadOnlyDictionary<OSPlatform, (string hash, string remoteSource)> ElmExecutableFileByOs =
ImmutableDictionary<OSPlatform, (string hash, string remoteSource)>.Empty
Expand Down
Loading

0 comments on commit 89f8847

Please sign in to comment.