Skip to content

Commit

Permalink
fix: stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
Rikarin committed Nov 1, 2023
1 parent 7be511c commit 6661d3c
Show file tree
Hide file tree
Showing 5 changed files with 302 additions and 72 deletions.
137 changes: 66 additions & 71 deletions Rin.BuildEngine.Common/Builder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,45 @@ namespace Rin.BuildEngine.Common;

public class Builder : IDisposable {
public const int ExpectedVersion = 4;

public const string MonitorPipeName = "Rin/BuildEngine/Monitor";
public static readonly string DoNotCompressTag = "DoNotCompress";
public static readonly string DoNotPackTag = "DoNotPack";

public readonly ISet<ObjectId> DisableCompressionIds = new HashSet<ObjectId>();


/// <summary>
/// The full path of the index file from the build directory.
/// The path on the disk where to perform the build
/// </summary>
string IndexFileFullPath =>
indexName != null
? VirtualFileSystem.ApplicationDatabasePath + VirtualFileSystem.DirectorySeparatorChar + indexName
: null;
readonly string buildPath;

public const string MonitorPipeName = "Stride/BuildEngine/Monitor";
/// <summary>
/// The name on the disk of the index file name.
/// </summary>
readonly string indexName;

public readonly ISet<ObjectId> DisableCompressionIds = new HashSet<ObjectId>();
readonly CommandIOMonitor ioMonitor;
readonly DateTime startTime;
readonly StepCounter stepCounter = new();

/// <summary>
/// Cancellation token source used for cancellation.
/// </summary>
CancellationTokenSource cancellationTokenSource;

/// <summary>
/// A map containing results of each commands, indexed by command hashes. When the builder is running, this map if
/// filled with the result of the commands of the current execution.
/// </summary>
ObjectDatabase resultMap;

/// <summary>
/// The build mode of the current run execution
/// </summary>
Mode runMode;

Scheduler scheduler;

/// <summary>
/// Gets the <see cref="ObjectDatabase" /> in which built objects are written.
Expand Down Expand Up @@ -74,59 +99,12 @@ public class Builder : IDisposable {
public CommandBuildStep.TryExecuteRemoteDelegate TryExecuteRemote { get; set; }

/// <summary>
/// Indicate which mode to use with this builder
/// </summary>
public enum Mode {
/// <summary>
/// Build the script
/// </summary>
Build,

/// <summary>
/// Clean the command cache used to determine wheither a command has already been triggered.
/// </summary>
Clean,

/// <summary>
/// Clean the command cache and delete every output objects
/// </summary>
CleanAndDelete
}


/// <summary>
/// The path on the disk where to perform the build
/// </summary>
readonly string buildPath;

/// <summary>
/// The name on the disk of the index file name.
/// </summary>
readonly string indexName;

readonly CommandIOMonitor ioMonitor;

readonly DateTime startTime;

readonly StepCounter stepCounter = new StepCounter();

/// <summary>
/// Cancellation token source used for cancellation.
/// </summary>
CancellationTokenSource cancellationTokenSource;

/// <summary>
/// A map containing results of each commands, indexed by command hashes. When the builder is running, this map if
/// filled with the result of the commands of the current execution.
/// </summary>
ObjectDatabase resultMap;

/// <summary>
/// The build mode of the current run execution
/// The full path of the index file from the build directory.
/// </summary>
Mode runMode;

Scheduler scheduler;
string IndexFileFullPath =>
indexName != null
? VirtualFileSystem.ApplicationDatabasePath + VirtualFileSystem.DirectorySeparatorChar + indexName
: null;


public Builder(ILogger logger, string buildPath, string indexName) {
Expand All @@ -136,7 +114,7 @@ public Builder(ILogger logger, string buildPath, string indexName) {
Logger = logger;
this.buildPath = buildPath ?? throw new ArgumentNullException(nameof(buildPath));
Root = new();
ioMonitor = new CommandIOMonitor(Logger);
ioMonitor = new(Logger);
ThreadCount = Environment.ProcessorCount;
BuilderId = Guid.NewGuid();
InitialVariables = new Dictionary<string, string>();
Expand Down Expand Up @@ -188,7 +166,6 @@ public BuildResultCode Run(Mode mode, bool writeIndexFile = true) {
OpenObjectDatabase(buildPath, indexName);

PreRun();

runMode = mode;

if (IsRunning) {
Expand All @@ -205,9 +182,7 @@ public BuildResultCode Run(Mode mode, bool writeIndexFile = true) {
var inputHashes = FileVersionTracker.GetDefault();
{
var builderContext = new BuilderContext(inputHashes, TryExecuteRemote);

resultMap = ObjectDatabase;

scheduler = new();

// Schedule the build
Expand Down Expand Up @@ -240,15 +215,15 @@ public BuildResultCode Run(Mode mode, bool writeIndexFile = true) {
} else if (stepCounter.Get(ResultStatus.Failed) > 0
|| stepCounter.Get(ResultStatus.NotTriggeredPrerequisiteFailed) > 0) {
Logger.Error(
$"Build finished in {stepCounter.Total} steps. Command results: {stepCounter.Get(ResultStatus.Successful)} succeeded, {stepCounter.Get(ResultStatus.NotTriggeredWasSuccessful)} up-to-date, {stepCounter.Get(ResultStatus.Failed)} failed, {stepCounter.Get(ResultStatus.NotTriggeredPrerequisiteFailed)} not triggered due to previous failure."
$"Build finished in {stepCounter.Total} steps. Command results: {stepCounter.Get(ResultStatus.Successful)} succeeded, {stepCounter.Get(ResultStatus.NotTriggeredWasSuccessful)} up-to-date, {stepCounter.Get(ResultStatus.Failed)} failed, {stepCounter.Get(ResultStatus.NotTriggeredPrerequisiteFailed)} not triggered due to previous failure"
);
Logger.Error("Build failed.");
result = BuildResultCode.BuildError;
} else {
Logger.Info(
$"Build finished in {stepCounter.Total} steps. Command results: {stepCounter.Get(ResultStatus.Successful)} succeeded, {stepCounter.Get(ResultStatus.NotTriggeredWasSuccessful)} up-to-date, {stepCounter.Get(ResultStatus.Failed)} failed, {stepCounter.Get(ResultStatus.NotTriggeredPrerequisiteFailed)} not triggered due to previous failure."
Logger.Information(
$"Build finished in {stepCounter.Total} steps. Command results: {stepCounter.Get(ResultStatus.Successful)} succeeded, {stepCounter.Get(ResultStatus.NotTriggeredWasSuccessful)} up-to-date, {stepCounter.Get(ResultStatus.Failed)} failed, {stepCounter.Get(ResultStatus.NotTriggeredPrerequisiteFailed)} not triggered due to previous failure"
);
Logger.Info("Build is successful.");
Logger.Information("Build is successful");
result = BuildResultCode.Successful;
}
} else {
Expand All @@ -267,14 +242,14 @@ public BuildResultCode Run(Mode mode, bool writeIndexFile = true) {
}

if (cancellationTokenSource.IsCancellationRequested) {
Logger.Error(modeName + " has been cancelled.");
Logger.Error(modeName + " has been cancelled");
result = BuildResultCode.Cancelled;
} else if (stepCounter.Get(ResultStatus.Failed) > 0
|| stepCounter.Get(ResultStatus.NotTriggeredPrerequisiteFailed) > 0) {
Logger.Error(modeName + " has failed.");
Logger.Error(modeName + " has failed");
result = BuildResultCode.BuildError;
} else {
Logger.Error(modeName + " has been successfully completed.");
Logger.Error(modeName + " has been successfully completed");
result = BuildResultCode.Successful;
}
}
Expand Down Expand Up @@ -407,7 +382,7 @@ void ComputeDependencyGraph(Dictionary<string, KeyValuePair<BuildStep, HashSet<s
/// </summary>
/// <param name="rootStep">The root BuildStep</param>
void GenerateDependencies(BuildStep rootStep) {
// TODO: Support proper incremental dependecies
// TODO: Support proper incremental dependencies
if (rootStep.ProcessedDependencies) {
return;
}
Expand Down Expand Up @@ -640,6 +615,26 @@ IDictionary<string, string> variables
}
}

/// <summary>
/// Indicate which mode to use with this builder
/// </summary>
public enum Mode {
/// <summary>
/// Build the script
/// </summary>
Build,

/// <summary>
/// Clean the command cache used to determine wheither a command has already been triggered.
/// </summary>
Clean,

/// <summary>
/// Clean the command cache and delete every output objects
/// </summary>
CleanAndDelete
}

class ExecuteContext : IExecuteContext {
readonly Builder builder;
readonly BuilderContext builderContext;
Expand Down
160 changes: 160 additions & 0 deletions Rin.BuildEngine.Common/CommandIOMonitor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using Rin.Core.Storage;
using Rin.Core.TODO;
using Serilog;
using System.Diagnostics;

namespace Rin.BuildEngine.Common;

/// <summary>
/// This class monitors input/output access from every BuildStep execution, and display an error message if an object
/// url is the input of a command and the output of another command running at the same time.
/// </summary>
class CommandIOMonitor {
/// <summary>
/// A dictionary containing read and write access timings (value) of a given object url (key)
/// </summary>
readonly Dictionary<ObjectUrl, ObjectAccesses> objectsAccesses = new();

/// <summary>
/// A dictionary containing execution intervals of BuildStep
/// </summary>
readonly Dictionary<CommandBuildStep, TimeInterval> commandExecutionIntervals = new();

readonly Dictionary<CommandBuildStep, List<ObjectUrl>> commandInputFiles = new();

readonly ILogger logger;

readonly object lockObject = new();

readonly Stopwatch stopWatch = new();

// Store earliest start time of command still running (to clean up accesses as time goes)
long earliestCommandAliveStartTime;

public CommandIOMonitor(ILogger logger) {
this.logger = logger;
stopWatch.Start();
}

public void CommandStarted(CommandBuildStep command) {
lock (lockObject) {
var startTime = stopWatch.ElapsedTicks;
commandExecutionIntervals.Add(command, new TimeInterval(startTime));

// Get a list of unique input files
var inputFiles = command.Command.GetInputFiles().Distinct().ToList();
// Store it aside, so that we're sure to remove the same entries during CommandEnded
commandInputFiles.Add(command, inputFiles);

// Setup start read time for each file entry
var inputHash = new HashSet<ObjectUrl>();
foreach (var inputUrl in inputFiles) {
if (inputHash.Contains(inputUrl)) {
logger.Error(
$"The command '{command.Title}' has several times the file '{inputUrl.Path}' as input. Input Files must not be duplicated"
);
}

inputHash.Add(inputUrl);

ObjectAccesses inputAccesses;
if (!objectsAccesses.TryGetValue(inputUrl, out inputAccesses)) {
objectsAccesses.Add(inputUrl, inputAccesses = new());
}

inputAccesses.Reads.Add(new TimeInterval<BuildStep>(command, startTime));
}
}
}

public void CommandEnded(CommandBuildStep command) {
lock (lockObject) {
TimeInterval commandInterval = commandExecutionIntervals[command];

long startTime = commandInterval.StartTime;
var endTime = stopWatch.ElapsedTicks;
commandInterval.End(endTime);

commandExecutionIntervals.Remove(command);

foreach (var outputObject in command.Result.OutputObjects) {
var outputUrl = outputObject.Key;
if (objectsAccesses.TryGetValue(outputUrl, out var inputAccess)) {
foreach (TimeInterval<BuildStep> input in inputAccess.Reads.Where(
input => input.Object != command && input.Overlap(startTime, endTime)
)) {
logger.Error(
$"Command {command} is writing {outputUrl} while command {input.Object} is reading it"
);
}
}

if (!objectsAccesses.TryGetValue(outputUrl, out var outputAccess)) {
objectsAccesses.Add(outputUrl, outputAccess = new());
}

foreach (var output in outputAccess.Writes.Where(
output => output.Object.Key != command && output.Overlap(startTime, endTime)
)) {
if (outputObject.Value != output.Object.Value) {
logger.Error(
$"Commands {command} and {output.Object} are both writing {outputUrl} at the same time, but they are different objects"
);
}
}

outputAccess.Writes.Add(
new TimeInterval<KeyValuePair<BuildStep, ObjectId>>(
new KeyValuePair<BuildStep, ObjectId>(command, outputObject.Value),
startTime,
endTime
)
);
}

foreach (var inputUrl in command.Result.InputDependencyVersions.Keys) {
if (objectsAccesses.TryGetValue(inputUrl, out var outputAccess)) {
foreach (TimeInterval<KeyValuePair<BuildStep, ObjectId>> output in outputAccess.Writes.Where(
output => output.Object.Key != command && output.Overlap(startTime, endTime)
)) {
logger.Error(
$"Command {output.Object} is writing {inputUrl} while command {command} is reading it"
);
}
}
}

// Notify that we're done reading input files
if (commandInputFiles.TryGetValue(command, out var inputFiles)) {
commandInputFiles.Remove(command);
foreach (var input in inputFiles) {
objectsAccesses[input].Reads.Single(x => x.Object == command).End(endTime);
}
}

// "Garbage collection" of accesses
var newEarliestCommandAliveStartTime = commandExecutionIntervals.Count > 0
? commandExecutionIntervals.Min(x => x.Value.StartTime)
: endTime;
if (newEarliestCommandAliveStartTime > earliestCommandAliveStartTime) {
earliestCommandAliveStartTime = newEarliestCommandAliveStartTime;

// We can remove objects whose all R/W accesses are "completed" (EndTime is set)
// and happened before all the current running commands started, since they won't affect us
foreach (var objectAccesses in objectsAccesses.ToList()) {
if (objectAccesses.Value.Reads.All(x => x.EndTime != 0 && x.EndTime < earliestCommandAliveStartTime)
&& objectAccesses.Value.Writes.All(
x => x.EndTime != 0 && x.EndTime < earliestCommandAliveStartTime
)) {
objectsAccesses.Remove(objectAccesses.Key);
}
}
}
}
}

class ObjectAccesses {
public List<TimeInterval<BuildStep>> Reads { get; } = new();
public List<TimeInterval<KeyValuePair<BuildStep, ObjectId>>> Writes { get; } = new();
}
}
Loading

0 comments on commit 6661d3c

Please sign in to comment.