Release FileAccessMemory after starting the process (#1182)

This commit is contained in:
Aleksandar Milicevic 2019-11-09 15:44:29 -08:00 коммит произвёл GitHub
Родитель 61afc4ef81
Коммит 683881171e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 170 добавлений и 79 удалений

Просмотреть файл

@ -900,6 +900,32 @@ namespace BuildXL.Processes
}
}
/// <summary>
/// Explicitly releases most of its memory.
/// </summary>
internal void Release()
{
m_normalizedFragments?.Clear();
var workList = new Stack<Node>();
workList.Push(m_rootNode);
while (workList.Count > 0)
{
var node = workList.Pop();
if (node == null)
{
continue;
}
if (node.Children != null)
{
foreach (var child in node.Children)
{
workList.Push(child);
}
}
node.ReleaseChildren();
}
}
/// <summary>
/// Serializes this manifest.
/// </summary>
@ -1249,6 +1275,14 @@ namespace BuildXL.Processes
/// </summary>
internal IEnumerable<Node> Children => m_children?.Values;
/// <summary>
/// Releases all children nodes, allowing all that memory to be reclaimed by the garbage collector.
/// </summary>
internal void ReleaseChildren()
{
m_children?.Clear();
}
/// <summary>
/// The path ID as understood by the owning path table.
/// </summary>

Просмотреть файл

@ -58,20 +58,19 @@ namespace BuildXL.Processes
private long m_processKilledFlag = 0;
/// <summary>
/// Returns the associated PipId
/// </summary>
private long PipId => ProcessInfo.FileAccessManifest.PipId;
private ulong m_processExitTimeNs = ulong.MaxValue;
private bool HasProcessExitBeenReceived => m_processExitTimeNs != ulong.MaxValue;
private readonly CancellationTokenSource m_timeoutTaskCancelationSource = new CancellationTokenSource();
private ISandboxConnection SandboxConnection => ProcessInfo.SandboxConnection;
private long PipId { get; }
private TimeSpan ChildProcessTimeout => ProcessInfo.NestedProcessTerminationTimeout;
private ISandboxConnection SandboxConnection { get; }
private TimeSpan ChildProcessTimeout { get; }
private TimeSpan? ReportQueueProcessTimeoutForTests { get; }
/// <summary>
/// Accumulates the time (in microseconds) access reports spend in the report queue
@ -87,7 +86,7 @@ namespace BuildXL.Processes
/// Timeout period for inactivity from the sandbox kernel extension.
/// </summary>
internal TimeSpan ReportQueueProcessTimeout => SandboxConnection.IsInTestMode
? ProcessInfo.ReportQueueProcessTimeoutForTests ?? TimeSpan.FromSeconds(100)
? ReportQueueProcessTimeoutForTests ?? TimeSpan.FromSeconds(100)
: TimeSpan.FromMinutes(45);
private Task m_processTreeTimeoutTask;
@ -95,7 +94,7 @@ namespace BuildXL.Processes
/// <summary>
/// Allowed surviving child process names.
/// </summary>
private string[] AllowedSurvivingChildProcessNames => ProcessInfo.AllowedSurvivingChildProcessNames;
private string[] AllowedSurvivingChildProcessNames { get; }
private bool IgnoreReportedAccesses { get; }
@ -114,6 +113,11 @@ namespace BuildXL.Processes
Contract.Requires(info.FileAccessManifest != null);
Contract.Requires(info.SandboxConnection != null);
PipId = info.FileAccessManifest.PipId;
SandboxConnection = info.SandboxConnection;
ChildProcessTimeout = info.NestedProcessTerminationTimeout;
AllowedSurvivingChildProcessNames = info.AllowedSurvivingChildProcessNames;
ReportQueueProcessTimeoutForTests = info.ReportQueueProcessTimeoutForTests;
IgnoreReportedAccesses = ignoreReportedAccesses;
MeasureCpuTime = overrideMeasureTime.HasValue
@ -145,7 +149,7 @@ namespace BuildXL.Processes
});
// install a 'ProcessStarted' handler that informs the sandbox of the newly started process
ProcessStarted += () => OnProcessStartedAsync().GetAwaiter().GetResult();
ProcessStarted += (pid) => OnProcessStartedAsync(info).GetAwaiter().GetResult();
}
private void UpdatePerfCounters()
@ -180,9 +184,9 @@ namespace BuildXL.Processes
}
/// <inheritdoc />
protected override System.Diagnostics.Process CreateProcess()
protected override System.Diagnostics.Process CreateProcess(SandboxedProcessInfo info)
{
var process = base.CreateProcess();
var process = base.CreateProcess(info);
process.StartInfo.FileName = "/bin/sh";
process.StartInfo.Arguments = string.Empty;
@ -199,7 +203,7 @@ namespace BuildXL.Processes
/// piped to its standard input. Therefore, in this handler we first notify the sandbox of the new process (so that
/// it starts tracking it) and then just send the actual process command line to /bin/sh via its standard input.
/// </summary>
private async Task OnProcessStartedAsync()
private async Task OnProcessStartedAsync(SandboxedProcessInfo info)
{
// Generate "Process Created" report because the rest of the system expects to see it before any other file access reports
//
@ -209,7 +213,7 @@ namespace BuildXL.Processes
ReportProcessCreated();
// Allow read access for /bin/sh
ProcessInfo.FileAccessManifest.AddPath(
info.FileAccessManifest.AddPath(
AbsolutePath.Create(PathTable, Process.StartInfo.FileName),
mask: FileAccessPolicy.MaskNothing,
values: FileAccessPolicy.AllowReadAlways);
@ -219,14 +223,16 @@ namespace BuildXL.Processes
m_perfCollector.Start();
}
if (!SandboxConnection.NotifyPipStarted(ProcessInfo.FileAccessManifest, this))
string processStdinFileName = await FlushStandardInputToFileIfNeededAsync(info);
if (!SandboxConnection.NotifyPipStarted(info.FileAccessManifest, this))
{
ThrowCouldNotStartProcess("Failed to notify kernel extension about process start, make sure the extension is loaded");
}
try
{
await FeedStdInAsync();
await FeedStdInAsync(info, processStdinFileName);
m_processTreeTimeoutTask = ProcessTreeTimeoutTask();
}
catch (IOException e)
@ -236,6 +242,14 @@ namespace BuildXL.Processes
LogProcessState($"IOException caught while feeding the standard input: {e.ToString()}");
await KillAsync();
}
finally
{
// release the FileAccessManifest memory
// NOTE: just by not keeping any references to 'info' should make the FileAccessManifest object
// unreachable and thus available for garbage collection. We call Release() here explicitly
// just to emphasise the importance of reclaiming this memory.
info.FileAccessManifest.Release();
}
}
/// <inheritdoc />
@ -375,13 +389,12 @@ namespace BuildXL.Processes
}
}
private async Task FeedStdInAsync()
private async Task FeedStdInAsync(SandboxedProcessInfo info, [CanBeNull] string processStdinFileName)
{
string processStdinFileName = await FlushStandardInputToFileIfNeededAsync();
string redirectedStdin = processStdinFileName != null ? $" < {processStdinFileName}" : string.Empty;
string escapedArguments = ProcessInfo.Arguments.Replace(Environment.NewLine, "\\" + Environment.NewLine);
string escapedArguments = info.Arguments.Replace(Environment.NewLine, "\\" + Environment.NewLine);
string line = I($"exec {ProcessInfo.FileName} {escapedArguments} {redirectedStdin}");
string line = I($"exec {info.FileName} {escapedArguments} {redirectedStdin}");
LogProcessState("Feeding stdin: " + line);
await Process.StandardInput.WriteLineAsync(line);
@ -550,7 +563,7 @@ namespace BuildXL.Processes
private void HandleAccessReport(AccessReport report)
{
if (ProcessInfo.FileAccessManifest.ReportFileAccesses)
if (ShouldReportFileAccesses)
{
LogProcessState("Access report received: " + AccessReportToString(report));
}
@ -650,16 +663,16 @@ namespace BuildXL.Processes
/// If <see cref="SandboxedProcessInfo.StandardInputReader"/> is set, it flushes the content of that reader
/// to a file in the process's working directory and returns the absolute path of that file; otherwise returns null.
/// </summary>
private async Task<string> FlushStandardInputToFileIfNeededAsync()
private async Task<string> FlushStandardInputToFileIfNeededAsync(SandboxedProcessInfo info)
{
if (ProcessInfo.StandardInputReader == null)
if (info.StandardInputReader == null)
{
return null;
}
string stdinFileName = Path.Combine(Process.StartInfo.WorkingDirectory, ProcessInfo.PipSemiStableHash + ".stdin");
string stdinText = await ProcessInfo.StandardInputReader.ReadToEndAsync();
Encoding encoding = ProcessInfo.StandardInputEncoding ?? Console.InputEncoding;
string stdinFileName = Path.Combine(Process.StartInfo.WorkingDirectory, info.PipSemiStableHash + ".stdin");
string stdinText = await info.StandardInputReader.ReadToEndAsync();
Encoding encoding = info.StandardInputEncoding ?? Console.InputEncoding;
byte[] stdinBytes = encoding.GetBytes(stdinText);
bool stdinFileWritten = await FileUtilities.WriteAllBytesAsync(stdinFileName, stdinBytes);
if (!stdinFileWritten)
@ -668,7 +681,7 @@ namespace BuildXL.Processes
}
// Allow read from the created stdin file
ProcessInfo.FileAccessManifest.AddPath(AbsolutePath.Create(PathTable, stdinFileName), mask: FileAccessPolicy.MaskNothing, values: FileAccessPolicy.AllowRead);
info.FileAccessManifest.AddPath(AbsolutePath.Create(PathTable, stdinFileName), mask: FileAccessPolicy.MaskNothing, values: FileAccessPolicy.AllowRead);
return stdinFileName;
}

Просмотреть файл

@ -11,7 +11,7 @@ using System.Threading.Tasks;
using BuildXL.Interop;
using BuildXL.Utilities;
using BuildXL.Utilities.Configuration.Mutable;
using static BuildXL.Utilities.FormattableStringEx;
using BuildXL.Utilities.Instrumentation.Common;
using JetBrains.Annotations;
#if FEATURE_SAFE_PROCESS_HANDLE
using Microsoft.Win32.SafeHandles;
@ -22,6 +22,8 @@ using SafeProcessHandle = BuildXL.Interop.Windows.SafeProcessHandle;
using static BuildXL.Interop.MacOS.IO;
#endif
using static BuildXL.Utilities.FormattableStringEx;
namespace BuildXL.Processes
{
/// <summary>
@ -34,10 +36,9 @@ namespace BuildXL.Processes
private readonly SandboxedProcessOutputBuilder m_output;
private readonly SandboxedProcessOutputBuilder m_error;
private readonly AsyncProcessExecutor m_processExecutor;
private DateTime m_reportsReceivedTime;
private AsyncProcessExecutor m_processExecutor;
private Exception m_dumpCreationException;
internal class CpuTimes
@ -57,7 +58,7 @@ namespace BuildXL.Processes
/// <summary>
/// Delegate type for <see cref="ProcessStarted"/> event.
/// </summary>
protected delegate void ProcessStartedHandler();
protected delegate void ProcessStartedHandler(int processId);
/// <summary>
/// Raised right after the process is started.
@ -70,20 +71,35 @@ namespace BuildXL.Processes
/// </summary>
protected virtual bool Killed => m_processExecutor?.Killed ?? false;
/// <summary>
/// Process information associated with this process.
/// </summary>
protected SandboxedProcessInfo ProcessInfo { get; }
/// <summary>
/// Underlying managed <see cref="Process"/> object.
/// </summary>
protected Process Process => m_processExecutor?.Process;
/// <summary>
/// Logging context from the <see cref="SandboxedProcessInfo"/> object passed to the constructor.
/// </summary>
protected LoggingContext LoggingContext { get; }
/// <summary>
/// Pip description from the <see cref="SandboxedProcessInfo"/> object passed to the constructor.
/// </summary>
protected string PipDescription { get; }
/// <summary>
/// Pip's semi-stable hash from the <see cref="SandboxedProcessInfo"/> object passed to the constructor.
/// </summary>
protected long PipSemiStableHash { get; }
/// <summary>
/// Returns the path table from the supplied <see cref="SandboxedProcessInfo"/>.
/// </summary>
protected PathTable PathTable => ProcessInfo.PathTable;
protected PathTable PathTable { get; }
/// <summary>
/// Whether /logObservedFileAccesses has been requested
/// </summary>
protected bool ShouldReportFileAccesses { get; }
/// <summary>
/// Whether there were any failures regarding sandboxing (e.g., sandbox
@ -91,33 +107,59 @@ namespace BuildXL.Processes
/// </summary>
protected virtual bool HasSandboxFailures => false;
/// <nodoc />
private string TimeoutDumpDirectory { get; }
/// <remarks>
/// IMPORTANT: For memory efficiency reasons don't keep a reference to <paramref name="info"/>
/// or its <see cref="SandboxedProcessInfo.FileAccessManifest"/> property
/// (at least not after the process has been started)
/// </remarks>
public UnsandboxedProcess(SandboxedProcessInfo info)
{
Contract.Requires(info != null);
Started = false;
PathTable = info.PathTable;
LoggingContext = info.LoggingContext;
PipDescription = info.PipDescription;
PipSemiStableHash = info.PipSemiStableHash;
TimeoutDumpDirectory = info.TimeoutDumpDirectory;
ShouldReportFileAccesses = info.FileAccessManifest?.ReportFileAccesses == true;
info.Timeout = info.Timeout ?? DefaultProcessTimeout;
ProcessInfo = info;
m_output = new SandboxedProcessOutputBuilder(
ProcessInfo.StandardOutputEncoding ?? Console.OutputEncoding,
ProcessInfo.MaxLengthInMemory,
ProcessInfo.FileStorage,
info.StandardOutputEncoding ?? Console.OutputEncoding,
info.MaxLengthInMemory,
info.FileStorage,
SandboxedProcessFile.StandardOutput,
ProcessInfo.StandardOutputObserver);
info.StandardOutputObserver);
m_error = new SandboxedProcessOutputBuilder(
ProcessInfo.StandardErrorEncoding ?? Console.OutputEncoding,
ProcessInfo.MaxLengthInMemory,
ProcessInfo.FileStorage,
info.StandardErrorEncoding ?? Console.OutputEncoding,
info.MaxLengthInMemory,
info.FileStorage,
SandboxedProcessFile.StandardError,
ProcessInfo.StandardErrorObserver);
info.StandardErrorObserver);
m_processExecutor = new AsyncProcessExecutor(
CreateProcess(info),
info.Timeout ?? DefaultProcessTimeout,
line => FeedStdOut(m_output, line),
line => FeedStdErr(m_error, line),
info.Provenance,
msg => LogProcessState(msg));
if (info.ProcessIdListener != null)
{
ProcessStarted += (pid) => info.ProcessIdListener(pid);
}
}
/// <summary>
/// Whether this process has been started (i.e., the <see cref="Start"/> method has been called on it).
/// </summary>
public bool Started => Process != null;
public bool Started { get; private set; }
/// <summary>
/// Difference between now and when the process was started.
@ -133,19 +175,9 @@ namespace BuildXL.Processes
{
Contract.Requires(!Started, "Process was already started. Cannot start process more than once.");
m_processExecutor = new AsyncProcessExecutor(
CreateProcess(),
ProcessInfo.Timeout ?? DefaultProcessTimeout,
line => FeedStdOut(m_output, line),
line => FeedStdErr(m_error, line),
ProcessInfo.Provenance,
msg => LogProcessState(msg));
Started = true;
m_processExecutor.Start();
ProcessStarted?.Invoke();
ProcessInfo.ProcessIdListener?.Invoke(ProcessId);
ProcessStarted?.Invoke(ProcessId);
}
/// <inheritdoc />
@ -227,8 +259,7 @@ namespace BuildXL.Processes
await m_processExecutor.WaitForStdOutAndStdErrAsync();
var reportFileAccesses = ProcessInfo.FileAccessManifest?.ReportFileAccesses == true;
var fileAccesses = reportFileAccesses ? (reports?.FileAccesses ?? s_emptyFileAccessesSet) : null;
var fileAccesses = ShouldReportFileAccesses ? (reports?.FileAccesses ?? s_emptyFileAccessesSet) : null;
return new SandboxedProcessResult
{
@ -247,7 +278,7 @@ namespace BuildXL.Processes
Processes = CoalesceProcesses(reports?.Processes),
MessageProcessingFailure = reports?.MessageProcessingFailure,
DumpCreationException = m_dumpCreationException,
DumpFileDirectory = ProcessInfo.TimeoutDumpDirectory,
DumpFileDirectory = TimeoutDumpDirectory,
PrimaryProcessTimes = GetProcessTimes(),
SurvivingChildProcesses = CoalesceProcesses(GetSurvivingChildProcesses())
};
@ -269,7 +300,7 @@ namespace BuildXL.Processes
{
Contract.Requires(Started);
ProcessDumper.TryDumpProcessAndChildren(ProcessId, ProcessInfo.TimeoutDumpDirectory, out m_dumpCreationException);
ProcessDumper.TryDumpProcessAndChildren(ProcessId, TimeoutDumpDirectory, out m_dumpCreationException);
LogProcessState($"UnsandboxedProcess::KillAsync()");
return m_processExecutor.KillAsync();
@ -278,22 +309,22 @@ namespace BuildXL.Processes
/// <summary>
/// Creates a <see cref="Process"/>.
/// </summary>
protected virtual Process CreateProcess()
protected virtual Process CreateProcess(SandboxedProcessInfo info)
{
Contract.Requires(Process == null);
Contract.Requires(!Started);
#if !PLATFORM_WIN
var mode = GetFilePermissionsForFilePath(ProcessInfo.FileName, followSymlink: false);
var mode = GetFilePermissionsForFilePath(info.FileName, followSymlink: false);
if (mode < 0)
{
ThrowBuildXLException($"Process creation failed: File '{ProcessInfo.FileName}' not found", new Win32Exception(0x2));
ThrowBuildXLException($"Process creation failed: File '{info.FileName}' not found", new Win32Exception(0x2));
}
var filePermissions = checked((FilePermissions)mode);
FilePermissions exePermission = FilePermissions.S_IXUSR;
if (!filePermissions.HasFlag(exePermission))
{
SetFilePermissionsForFilePath(ProcessInfo.FileName, exePermission);
SetFilePermissionsForFilePath(info.FileName, exePermission);
}
#endif
@ -301,9 +332,9 @@ namespace BuildXL.Processes
{
StartInfo = new ProcessStartInfo
{
FileName = ProcessInfo.FileName,
Arguments = ProcessInfo.Arguments,
WorkingDirectory = ProcessInfo.WorkingDirectory,
FileName = info.FileName,
Arguments = info.Arguments,
WorkingDirectory = info.WorkingDirectory,
StandardErrorEncoding = m_output.Encoding,
StandardOutputEncoding = m_error.Encoding,
RedirectStandardError = true,
@ -315,9 +346,9 @@ namespace BuildXL.Processes
};
process.StartInfo.EnvironmentVariables.Clear();
if (ProcessInfo.EnvironmentVariables != null)
if (info.EnvironmentVariables != null)
{
foreach (var envKvp in ProcessInfo.EnvironmentVariables.ToDictionary())
foreach (var envKvp in info.EnvironmentVariables.ToDictionary())
{
process.StartInfo.EnvironmentVariables[envKvp.Key] = envKvp.Value;
}
@ -350,7 +381,7 @@ namespace BuildXL.Processes
protected void ThrowBuildXLException(string message, Exception inner = null)
{
Process?.StandardInput?.Close();
throw new BuildXLException($"[Pip{ProcessInfo.PipSemiStableHash} -- {ProcessInfo.PipDescription}] {message}", inner);
throw new BuildXLException($"[Pip{PipSemiStableHash:X} -- {PipDescription}] {message}", inner);
}
/// <nodoc />
@ -359,10 +390,10 @@ namespace BuildXL.Processes
/// <nodoc />
protected void LogProcessState(string message)
{
if (ProcessInfo.LoggingContext != null)
if (LoggingContext != null)
{
string fullMessage = I($"Exited: {m_processExecutor?.ExitCompleted ?? false}, StdOut: {m_processExecutor?.StdOutCompleted ?? false}, StdErr: {m_processExecutor?.StdErrCompleted ?? false}, Reports: {ReportsCompleted()} :: {message}");
Tracing.Logger.Log.LogDetoursDebugMessage(ProcessInfo.LoggingContext, ProcessInfo.PipSemiStableHash, ProcessInfo.PipDescription, fullMessage);
Tracing.Logger.Log.LogDetoursDebugMessage(LoggingContext, PipSemiStableHash, PipDescription, fullMessage);
}
}

17
bxl.sh
Просмотреть файл

@ -10,7 +10,8 @@ source "$MY_DIR/Public/Src/Sandbox/MacOs/scripts/env.sh"
declare arg_Positional=()
declare arg_DeployDev=""
declare arg_UseDev=()
declare arg_DeployDevRelease=""
declare arg_UseDev=""
declare arg_Minimal=""
declare arg_Internal=""
@ -97,6 +98,10 @@ function parseArgs() {
arg_DeployDev="1"
shift
;;
--deploy-dev-release)
arg_DeployDevRelease="1"
shift
;;
--use-dev)
arg_UseDev="1"
shift
@ -123,7 +128,7 @@ function deployBxl { # (fromDir, toDir)
mkdir -p "$toDir"
/usr/bin/rsync -arhq "$fromDir/" "$toDir" --delete
print_info "Successfully deployed developer build to: $toDir; use it with the '--use-dev' flag now."
print_info "Successfully deployed developer build from $fromDir to: $toDir; use it with the '--use-dev' flag now."
}
parseArgs "$@"
@ -134,6 +139,10 @@ if [[ -n "$arg_DeployDev" || -n "$arg_Minimal" ]]; then
setMinimal
fi
if [[ -n "$arg_DeployDevRelease" ]]; then
arg_Positional+=(/q:ReleaseDotNetCoreMac "/f:output='$MY_DIR/Out/bin/release/osx-x64/*'")
fi
if [[ -n "$arg_Internal" ]]; then
setInternal $@
fi
@ -155,4 +164,8 @@ if [[ -n "$arg_DeployDev" ]]; then
deployBxl "$MY_DIR/Out/Bin/debug/osx-x64" "$MY_DIR/Out/Selfhost/Dev"
fi
if [[ -n "$arg_DeployDevRelease" ]]; then
deployBxl "$MY_DIR/Out/Bin/release/osx-x64" "$MY_DIR/Out/Selfhost/Dev"
fi
popd