Skip to content

Commit

Permalink
Reduce runtime expenses when garbage-collecting in the process store
Browse files Browse the repository at this point in the history
Reduce processing time for the `truncate-process-history` command: Specialize implementation for this entry point: Separate the loading of composition events and their dependencies from their application to restore the volatile representation. The truncate-process-history method only needs the former part. The latter is not necessary in this case.

One of the instances in production with version 2021-01-16 frequently reported more than 25 seconds duration via `getFilesForRestoreTimeSpentMilli`.
  • Loading branch information
Viir committed Jan 29, 2021
1 parent 6f3aa0f commit b827411
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Text;
using System.Text.RegularExpressions;
using Kalmit.PersistentProcess.WebHost.ProcessStoreSupportingMigrations;
using Newtonsoft.Json;
Expand Down Expand Up @@ -47,7 +47,18 @@ public class PersistentProcessVolatileRepresentation : IPersistentProcess, IDisp

public readonly Result<string, string> lastSetElmAppStateResult;

class LoadedReduction
public struct CompositionLogRecordWithLoadedDependencies
{
public CompositionLogRecordInFile compositionRecord;

public string compositionRecordHashBase16;

public ReductionWithLoadedDependencies? reduction;

public CompositionEventWithLoadedDependencies? composition;
}

public struct ReductionWithLoadedDependencies
{
public byte[] elmAppState;

Expand All @@ -56,6 +67,17 @@ class LoadedReduction
public Composition.TreeWithStringPath appConfigAsTree;
}

public struct CompositionEventWithLoadedDependencies
{
public byte[] UpdateElmAppStateForEvent;

public byte[] SetElmAppState;

public Composition.TreeWithStringPath DeployAppConfigAndInitElmAppState;

public Composition.TreeWithStringPath DeployAppConfigAndMigrateElmAppState;
}

static public (IDisposableProcessWithStringInterface process,
(string javascriptFromElmMake, string javascriptPreparedToRun) buildArtifacts,
IReadOnlyList<string> log)
Expand Down Expand Up @@ -120,21 +142,17 @@ static public (IImmutableDictionary<IImmutableList<string>, IImmutableList<byte>
}
};

/*
Following part can be made less expensive when we have an implementation that evaluates all readings but skips the
rest of the restoring. Something like an extension of `EnumerateCompositionLogRecordsForRestoreProcess` resolving all
dependencies on the store.
*/
using (var restoredProcess = Restore(new ProcessStoreReaderInFileStore(recordingReader), _ => { }))
{
return (
files: filesForProcessRestore.ToImmutableDictionary(EnumerableExtension.EqualityComparer<string>()),
lastCompositionLogRecordHashBase16: restoredProcess.lastCompositionLogRecordHashBase16);
}
var compositionLogRecords =
EnumerateCompositionLogRecordsForRestoreProcessAndLoadDependencies(new ProcessStoreReaderInFileStore(recordingReader))
.ToImmutableList();

return (
files: filesForProcessRestore.ToImmutableDictionary(EnumerableExtension.EqualityComparer<string>()),
lastCompositionLogRecordHashBase16: compositionLogRecords.LastOrDefault().compositionRecordHashBase16);
}

static IEnumerable<(CompositionLogRecordInFile compositionRecord, string compositionRecordHashBase16, LoadedReduction reduction)>
EnumerateCompositionLogRecordsForRestoreProcess(IProcessStoreReader storeReader) =>
static IEnumerable<CompositionLogRecordWithLoadedDependencies>
EnumerateCompositionLogRecordsForRestoreProcessAndLoadDependencies(IProcessStoreReader storeReader) =>
storeReader
.EnumerateSerializedCompositionLogRecordsReverse()
.Select(serializedCompositionLogRecord =>
Expand All @@ -147,7 +165,7 @@ dependencies on the store.

var reductionRecord = storeReader.LoadProvisionalReduction(compositionRecordHashBase16);

LoadedReduction loadedReduction = null;
ReductionWithLoadedDependencies? reduction = null;

if (reductionRecord?.appConfig?.HashBase16 != null && reductionRecord?.elmAppState?.HashBase16 != null)
{
Expand All @@ -169,7 +187,7 @@ dependencies on the store.
throw new Exception("Unexpected content of elmAppStateComponent " + reductionRecord.elmAppState?.HashBase16 + ": This is not a blob.");
}

loadedReduction = new LoadedReduction
reduction = new ReductionWithLoadedDependencies
{
appConfig = appConfigComponent,
appConfigAsTree = parseAppConfigAsTree.Ok,
Expand All @@ -178,14 +196,18 @@ dependencies on the store.
}
}

return (
compositionRecord: compositionRecord,
compositionRecordHashBase16: compositionRecordHashBase16,
reduction: loadedReduction);
return new CompositionLogRecordWithLoadedDependencies
{
compositionRecord = compositionRecord,
compositionRecordHashBase16 = compositionRecordHashBase16,
composition = LoadCompositionEventDependencies(compositionRecord.compositionEvent, storeReader),
reduction = reduction,
};
})
.TakeUntil(compositionAndReduction => compositionAndReduction.reduction != null);
.TakeUntil(compositionAndReduction => compositionAndReduction.reduction != null)
.Reverse();

static public PersistentProcessVolatileRepresentation Restore(
static public PersistentProcessVolatileRepresentation LoadFromStoreAndRestoreProcess(
IProcessStoreReader storeReader,
Action<string> logger,
ElmAppInterfaceConfig? overrideElmAppInterfaceConfig = null)
Expand All @@ -194,11 +216,11 @@ static public PersistentProcessVolatileRepresentation Restore(

logger?.Invoke("Begin to restore the process state.");

var compositionEventsToLatestReductionReversed =
EnumerateCompositionLogRecordsForRestoreProcess(storeReader)
var compositionEventsFromLatestReduction =
EnumerateCompositionLogRecordsForRestoreProcessAndLoadDependencies(storeReader)
.ToImmutableList();

if (!compositionEventsToLatestReductionReversed.Any())
if (!compositionEventsFromLatestReduction.Any())
{
logger?.Invoke("Found no composition record, default to initial state.");

Expand All @@ -209,15 +231,28 @@ static public PersistentProcessVolatileRepresentation Restore(
lastSetElmAppStateResult: null);
}

logger?.Invoke("Found " + compositionEventsToLatestReductionReversed.Count + " composition log records to use for restore.");
logger?.Invoke("Found " + compositionEventsFromLatestReduction.Count + " composition log records to use for restore.");

var processVolatileRepresentation = RestoreFromCompositionEventSequence(
compositionEventsFromLatestReduction,
overrideElmAppInterfaceConfig);

logger?.Invoke("Restored the process state in " + ((int)restoreStopwatch.Elapsed.TotalSeconds) + " seconds.");

return processVolatileRepresentation;
}

var firstCompositionEventRecord =
compositionEventsToLatestReductionReversed.LastOrDefault();
static public PersistentProcessVolatileRepresentation RestoreFromCompositionEventSequence(
IEnumerable<CompositionLogRecordWithLoadedDependencies> compositionLogRecords,
ElmAppInterfaceConfig? overrideElmAppInterfaceConfig = null)
{
var firstCompositionLogRecord =
compositionLogRecords.FirstOrDefault();

if (firstCompositionEventRecord.reduction == null &&
firstCompositionEventRecord.compositionRecord.parentHashBase16 != CompositionLogRecordInFile.compositionLogFirstRecordParentHashBase16)
if (firstCompositionLogRecord.reduction == null &&
firstCompositionLogRecord.compositionRecord.parentHashBase16 != CompositionLogRecordInFile.compositionLogFirstRecordParentHashBase16)
{
throw new Exception("Failed to get sufficient history: Composition log record points to parent " + firstCompositionEventRecord.compositionRecord.parentHashBase16);
throw new Exception("Failed to get sufficient history: Composition log record points to parent " + firstCompositionLogRecord.compositionRecord.parentHashBase16);
}

string lastCompositionLogRecordHashBase16 = null;
Expand All @@ -227,7 +262,7 @@ static public PersistentProcessVolatileRepresentation Restore(
lastElmAppVolatileProcess: null,
lastSetElmAppStateResult: null);

foreach (var compositionLogRecord in compositionEventsToLatestReductionReversed.Reverse())
foreach (var compositionLogRecord in compositionLogRecords)
{
try
{
Expand All @@ -237,17 +272,17 @@ static public PersistentProcessVolatileRepresentation Restore(
{
var (newElmAppProcess, (javascriptFromElmMake, javascriptPreparedToRun), _) =
ProcessFromWebAppConfig(
compositionLogRecord.reduction.appConfigAsTree,
compositionLogRecord.reduction.Value.appConfigAsTree,
overrideElmAppInterfaceConfig: overrideElmAppInterfaceConfig);

var elmAppStateAsString = Encoding.UTF8.GetString(compositionLogRecord.reduction.elmAppState);
var elmAppStateAsString = Encoding.UTF8.GetString(compositionLogRecord.reduction.Value.elmAppState);

newElmAppProcess.SetSerializedState(elmAppStateAsString);

processRepresentationDuringRestore?.lastElmAppVolatileProcess?.Dispose();

processRepresentationDuringRestore = new PersistentProcessVolatileRepresentationDuringRestore(
lastAppConfig: (compositionLogRecord.reduction.appConfig, (javascriptFromElmMake, javascriptPreparedToRun)),
lastAppConfig: (compositionLogRecord.reduction.Value.appConfig, (javascriptFromElmMake, javascriptPreparedToRun)),
lastElmAppVolatileProcess: newElmAppProcess,
lastSetElmAppStateResult: null);

Expand All @@ -269,9 +304,8 @@ static public PersistentProcessVolatileRepresentation Restore(

processRepresentationDuringRestore =
ApplyCompositionEvent(
compositionEvent,
compositionLogRecord.composition.Value,
processRepresentationDuringRestore,
storeReader,
overrideElmAppInterfaceConfig);
}
finally
Expand All @@ -280,8 +314,6 @@ static public PersistentProcessVolatileRepresentation Restore(
}
}

logger?.Invoke("Restored the process state in " + ((int)restoreStopwatch.Elapsed.TotalSeconds) + " seconds.");

return new PersistentProcessVolatileRepresentation(
lastCompositionLogRecordHashBase16: lastCompositionLogRecordHashBase16,
lastAppConfig: processRepresentationDuringRestore.lastAppConfig,
Expand Down Expand Up @@ -316,47 +348,17 @@ public PersistentProcessVolatileRepresentationDuringRestore WithLastSetElmAppSta
}

static PersistentProcessVolatileRepresentationDuringRestore ApplyCompositionEvent(
CompositionLogRecordInFile.CompositionEvent compositionEvent,
CompositionEventWithLoadedDependencies compositionEvent,
PersistentProcessVolatileRepresentationDuringRestore processBefore,
IProcessStoreReader storeReader,
ElmAppInterfaceConfig? overrideElmAppInterfaceConfig)
{
IImmutableList<byte> loadComponentFromStoreAndAssertIsBlob(string componentHash)
{
var component = storeReader.LoadComponent(componentHash);

if (component == null)
throw new Exception("Failed to load component " + componentHash + ": Not found in store.");

if (component.BlobContent == null)
throw new Exception("Failed to load component " + componentHash + " as blob: This is not a blob.");

return component.BlobContent;
}

Composition.TreeWithStringPath loadComponentFromStoreAndAssertIsTree(string componentHash)
{
var component = storeReader.LoadComponent(componentHash);

if (component == null)
throw new Exception("Failed to load component " + componentHash + ": Not found in store.");

var parseAsTreeResult = Composition.ParseAsTreeWithStringPath(component);

if (parseAsTreeResult.Ok == null)
throw new Exception("Failed to load component " + componentHash + " as tree: Failed to parse as tree.");

return parseAsTreeResult.Ok;
}

if (compositionEvent.UpdateElmAppStateForEvent != null)
{
if (processBefore.lastElmAppVolatileProcess == null)
return processBefore;

processBefore.lastElmAppVolatileProcess.ProcessEvent(
Encoding.UTF8.GetString(loadComponentFromStoreAndAssertIsBlob(
compositionEvent.UpdateElmAppStateForEvent.HashBase16).ToArray()));
Encoding.UTF8.GetString(compositionEvent.UpdateElmAppStateForEvent));

return processBefore;
}
Expand All @@ -374,8 +376,7 @@ Composition.TreeWithStringPath loadComponentFromStoreAndAssertIsTree(string comp
}

var projectedElmAppState =
Encoding.UTF8.GetString(loadComponentFromStoreAndAssertIsBlob(
compositionEvent.SetElmAppState.HashBase16).ToArray());
Encoding.UTF8.GetString(compositionEvent.SetElmAppState);

processBefore.lastElmAppVolatileProcess.SetSerializedState(projectedElmAppState);

Expand Down Expand Up @@ -403,8 +404,7 @@ Composition.TreeWithStringPath loadComponentFromStoreAndAssertIsTree(string comp
{
var elmAppStateBefore = processBefore.lastElmAppVolatileProcess?.GetSerializedState();

var appConfig = loadComponentFromStoreAndAssertIsTree(
compositionEvent.DeployAppConfigAndMigrateElmAppState.HashBase16);
var appConfig = compositionEvent.DeployAppConfigAndMigrateElmAppState;

var prepareMigrateResult =
PrepareMigrateSerializedValue(destinationAppConfigTree: appConfig);
Expand Down Expand Up @@ -450,8 +450,7 @@ Composition.TreeWithStringPath loadComponentFromStoreAndAssertIsTree(string comp

if (compositionEvent.DeployAppConfigAndInitElmAppState != null)
{
var appConfig = loadComponentFromStoreAndAssertIsTree(
compositionEvent.DeployAppConfigAndInitElmAppState.HashBase16);
var appConfig = compositionEvent.DeployAppConfigAndInitElmAppState;

var (newElmAppProcess, buildArtifacts, _) =
ProcessFromWebAppConfig(
Expand All @@ -469,6 +468,80 @@ Composition.TreeWithStringPath loadComponentFromStoreAndAssertIsTree(string comp
throw new Exception("Unexpected shape of composition event: " + JsonConvert.SerializeObject(compositionEvent));
}

static CompositionEventWithLoadedDependencies? LoadCompositionEventDependencies(
CompositionLogRecordInFile.CompositionEvent compositionEvent,
IProcessStoreReader storeReader)
{
IImmutableList<byte> loadComponentFromStoreAndAssertIsBlob(string componentHash)
{
var component = storeReader.LoadComponent(componentHash);

if (component == null)
throw new Exception("Failed to load component " + componentHash + ": Not found in store.");

if (component.BlobContent == null)
throw new Exception("Failed to load component " + componentHash + " as blob: This is not a blob.");

return component.BlobContent;
}

Composition.TreeWithStringPath loadComponentFromStoreAndAssertIsTree(string componentHash)
{
var component = storeReader.LoadComponent(componentHash);

if (component == null)
throw new Exception("Failed to load component " + componentHash + ": Not found in store.");

var parseAsTreeResult = Composition.ParseAsTreeWithStringPath(component);

if (parseAsTreeResult.Ok == null)
throw new Exception("Failed to load component " + componentHash + " as tree: Failed to parse as tree.");

return parseAsTreeResult.Ok;
}

if (compositionEvent.UpdateElmAppStateForEvent != null)
{
return new CompositionEventWithLoadedDependencies
{
UpdateElmAppStateForEvent = loadComponentFromStoreAndAssertIsBlob(
compositionEvent.UpdateElmAppStateForEvent.HashBase16).ToArray(),
};
}

if (compositionEvent.SetElmAppState != null)
{
return new CompositionEventWithLoadedDependencies
{
SetElmAppState = loadComponentFromStoreAndAssertIsBlob(
compositionEvent.SetElmAppState.HashBase16).ToArray(),
};
}

if (compositionEvent.DeployAppConfigAndMigrateElmAppState != null)
{
return new CompositionEventWithLoadedDependencies
{
DeployAppConfigAndMigrateElmAppState = loadComponentFromStoreAndAssertIsTree(
compositionEvent.DeployAppConfigAndMigrateElmAppState.HashBase16),
};
}

if (compositionEvent.DeployAppConfigAndInitElmAppState != null)
{
return new CompositionEventWithLoadedDependencies
{
DeployAppConfigAndInitElmAppState = loadComponentFromStoreAndAssertIsTree(
compositionEvent.DeployAppConfigAndInitElmAppState.HashBase16),
};
}

if (compositionEvent.RevertProcessTo != null)
return null;

throw new Exception("Unexpected shape of composition event: " + JsonConvert.SerializeObject(compositionEvent));
}

public class Result<ErrT, OkT>
{
public ErrT Err;
Expand All @@ -486,7 +559,7 @@ public class Result<ErrT, OkT>
compositionLogEvent: compositionLogEvent);

using (var projectedProcess =
Restore(new ProcessStoreReaderInFileStore(projectionResult.projectedReader), _ => { }))
LoadFromStoreAndRestoreProcess(new ProcessStoreReaderInFileStore(projectionResult.projectedReader), _ => { }))
{
if (compositionLogEvent.DeployAppConfigAndMigrateElmAppState != null ||
compositionLogEvent.SetElmAppState != null)
Expand Down
Loading

0 comments on commit b827411

Please sign in to comment.