|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// THE ASSEMBLY BUILT FROM THIS SOURCE FILE HAS BEEN DEPRECATED FOR YEARS. IT IS BUILT ONLY TO PROVIDE
// BACKWARD COMPATIBILITY FOR API USERS WHO HAVE NOT YET MOVED TO UPDATED APIS. PLEASE DO NOT SEND PULL
// REQUESTS THAT CHANGE THIS FILE WITHOUT FIRST CHECKING WITH THE MAINTAINERS THAT THE FIX IS REQUIRED.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using Microsoft.Build.Framework;
using Microsoft.Build.BuildEngine.Shared;
namespace Microsoft.Build.BuildEngine
{
/// <summary>
/// This class is responsible for representing the task execution subsystem to the engine. This
/// class can be instantiated in a different appdomain from the engine and doesn't share
/// any pointers/data with the engine(except for the argument to the functions).
/// </summary>
internal class TaskExecutionModule
{
#region Constructors
/// <summary>
/// The TaskExecutionModule is a the external view into a subsystem responsible for executing user
/// tasks. The subsystem consists of TaskWorkerThread, TaskEngine, TaskExecutionState and EngineProxy.
/// The engine thread passes the TaskExecutionState to the TEM which after the task finishes passes the
/// results back via the engineCallback.
/// </summary>
internal TaskExecutionModule
(
EngineCallback engineCallback,
TaskExecutionModuleMode moduleMode,
bool profileExecution
)
{
this.engineCallback = engineCallback;
this.moduleMode = moduleMode;
// By default start in breadthFirst traversal. This is done to gather enough work at the start of the build to get all the nodes at least working on something.
this.breadthFirstTraversal = true;
this.profileExecution = profileExecution;
this.totalTaskTime = 0;
// Get the node the TEM is running on, this is so the parent engine knows which node is requesting a traversal strategy change.
nodeId = engineCallback.GetParentEngine().NodeId;
SetBatchRequestSize();
// In singleproc mode the task execution module executes tasks on the engine thread. In multi proc mode a new thread is
// created so the TEM can submit tasks to a worker queue which will run the tasks on a new thread.
if (moduleMode != TaskExecutionModuleMode.SingleProcMode)
{
this.isRunningMultipleNodes = true;
this.activeThreadCount = 0;
this.overallThreadCount = 0;
this.threadActiveCountEvent = new ManualResetEvent(false);
this.threadOverallCountEvent = new ManualResetEvent(false);
this.lastTaskActivity = 0;
// Create a worker thread and make it the active node thread
workerThread = new TaskWorkerThread(this, profileExecution);
workerThread.ActivateThread();
}
else
{
this.isRunningMultipleNodes = false;
}
}
/// <summary>
/// Sets the requestBatch size based on an environment variable set by the user.
/// </summary>
private void SetBatchRequestSize()
{
// The RequestBatchSize is how many buildrequests will be sent to the parent engine at once when the system
// is running in multiproc mode. The idea of only sending a certain ammount of build requests was implemented
// due to the parent engine being flooded with build requests if the tree being built is very broad.
string requestBatchSizeEnvironmentVariable = Environment.GetEnvironmentVariable("MSBUILDREQUESTBATCHSIZE");
if (!String.IsNullOrEmpty(requestBatchSizeEnvironmentVariable))
{
if (!int.TryParse(requestBatchSizeEnvironmentVariable, out batchRequestSize) || batchRequestSize < 1)
{
// If an invalid RequestBatchSize is passed in set the batchRequestSize back to the default and log a warning.
batchRequestSize = defaultBatchRequestSize;
BuildEventContext buildEventContext = new BuildEventContext(
nodeId,
BuildEventContext.InvalidTargetId,
BuildEventContext.InvalidProjectContextId,
BuildEventContext.InvalidTaskId
);
engineCallback.GetParentEngine().LoggingServices.LogWarning(buildEventContext, new BuildEventFileInfo(/* there is truly no file associated with this warning */ String.Empty), "BatchRequestSizeOutOfRange", requestBatchSizeEnvironmentVariable);
}
}
}
#endregion
#region Properties
/// <summary>
/// This property allows a task to query whether or not the system is running in single process mode or multi process mode.
/// Single process mode (IsRunningMultipleNodes = false) is where the engine is initialized with the number of cpus = 1 and the engine is not a child engine.
/// The engine is in multi process mode (IsRunningMultipleNodes = true) when the engine is initialized with a number of cpus > 1 or the engine is a child engine.
/// </summary>
internal bool IsRunningMultipleNodes
{
get
{
return isRunningMultipleNodes;
}
}
/// <summary>
/// Specifies the traversal type for IBuildEngine callbacks. If true multiple build requests will be sent
/// to the engine if false the requests will be sent one at a time.
/// </summary>
internal bool UseBreadthFirstTraversal
{
get
{
return breadthFirstTraversal;
}
set
{
breadthFirstTraversal = value;
}
}
/// <summary>
/// Returns true if the TEM doesn't have a thread in user code and there are no pending
/// workitems
/// </summary>
internal bool IsIdle
{
get
{
return activeThreadCount == 0 && workerThread.WorkItemCount == 0;
}
}
/// <summary>
/// Return total time spent executing the tasks by this TEM. This value is only valid if the TEM is created with
/// profileExecution set to true, otherwise this value will be 0
/// </summary>
internal long TaskExecutionTime
{
get
{
return totalTaskTime;
}
}
#endregion
#region Method used internally inside the TEM boundary (i.e. not called from the engine)
/// <summary>
/// This method passes the task outputs to the engine, it is virtual for testing purposes to
/// create a mock TEM
/// </summary>
internal virtual void PostTaskOutputs
(
int handleId,
bool taskExecutedSuccessfully,
Exception thrownException,
long executionTime
)
{
totalTaskTime += executionTime;
engineCallback.PostTaskOutputs(handleId, taskExecutedSuccessfully, thrownException, executionTime);
}
/// <summary>
/// This function implements the callback via the IBuildEngine interface
/// </summary>
/// <returns>result of call to engine</returns>
internal virtual bool BuildProjectFile
(
int handleId,
string[] projectFileNames,
string[] targetNames,
IDictionary[] globalPropertiesPerProject,
IDictionary[] targetOutputsPerProject,
EngineLoggingServices loggingServices,
string[] toolsVersions,
bool useResultsCache,
bool unloadProjectsOnCompletion,
BuildEventContext taskContext
)
{
if (projectFileNames.Length == 0)
{
// Nothing to do, just return success
return true;
}
string currentDir = FileUtilities.GetCurrentDirectoryStaticBuffer(currentDirectoryBuffer);
if (Engine.debugMode)
{
string targetName = targetNames == null ? "null" : targetNames[0];
bool remoteNode = false;
for (int r = 0; r < projectFileNames.Length; r++)
{
string fullProjectName = projectFileNames[r] ?? "null";
Console.WriteLine("RemoteNode: " + remoteNode + " Project " + fullProjectName + " T:" + targetName + " NodeProdyId# " + handleId + " Time " + DateTime.Now.ToLongTimeString());
if (globalPropertiesPerProject[r] != null)
{
foreach (DictionaryEntry entry in globalPropertiesPerProject[r])
{
Console.WriteLine(currentDir + " :GLOBAL " + entry.Key + "=" + entry.Value.ToString());
}
}
}
}
BuildRequest[] buildRequests = new BuildRequest[projectFileNames.Length];
for (int i = 0; i < buildRequests.Length; i++)
{
// We need to get the full path to the project before we call back
// into the engine which has no control over current path
string fullProjectName = projectFileNames[i] != null ?
Path.GetFullPath(projectFileNames[i]) : null;
buildRequests[i] = new BuildRequest(handleId, fullProjectName, targetNames, globalPropertiesPerProject[i],
toolsVersions[i], i, useResultsCache, unloadProjectsOnCompletion);
ErrorUtilities.VerifyThrow(buildRequests[i].IsGeneratedRequest, "Should not be sending non generated requests from TEM to engine");
buildRequests[i].ParentBuildEventContext = taskContext;
}
BuildResult[] buildResultsLocal = new BuildResult[projectFileNames.Length];
if (moduleMode == TaskExecutionModuleMode.SingleProcMode)
{
for (int i = 0; i < projectFileNames.Length; i++)
{
// If we are running in a single threaded mode we need to
// re-enter the main build loop on the current thread in order
// to build the requested project, because the main build is below
// us on the stack
engineCallback.PostBuildRequestsToHost(new BuildRequest[] { buildRequests[i] });
buildResultsLocal[i] = engineCallback.GetParentEngine().EngineBuildLoop(buildRequests[i]);
buildResultsLocal[i].ConvertToTaskItems();
}
}
else
{
WaitForBuildResults(handleId, buildResultsLocal, buildRequests);
}
// Store the outputs in the hashtables provided by the caller
bool overallResult = true;
for (int i = 0; i < buildResultsLocal.Length; i++)
{
// Users of the Object Model can pass in null targetOutputs for projects they do not want outputs for
// therefore we need to make sure that there are targetoutputs and the users want the results
if (buildResultsLocal[i] != null)
{
if (buildResultsLocal[i].OutputsByTarget != null && targetOutputsPerProject[i] != null)
{
foreach (DictionaryEntry entry in buildResultsLocal[i].OutputsByTarget)
{
targetOutputsPerProject[i].Add(entry.Key, entry.Value);
}
overallResult = overallResult && buildResultsLocal[i].EvaluationResult;
}
}
else
{
// The calculation was terminated prior to receiving the result
overallResult = false;
}
}
// We're now returning from an IBuildEngine callback;
// set the current directory back to what the tasks expect
if (Directory.GetCurrentDirectory() != currentDir)
{
Directory.SetCurrentDirectory(currentDir);
}
if (Engine.debugMode)
{
bool remoteNode = false;
string targetName = targetNames == null ? "null" : targetNames[0];
Console.WriteLine("RemoteNode: " + remoteNode + " T:" + targetName + " HandleId# " + handleId + " Result " + overallResult);
}
return overallResult;
}
/// <summary>
/// Once the buildRequests from the EngineCallback have been created they are sent to this method which will
/// post the build requests to the parent engine and then wait on the results to come back.
/// This method uses either a breadthFirst or depthFirst traversal strategy when sending buildRequests to the parent engine.
/// This method will start in breadthFirst traversal. It will continue to use this strategy until one of two events occur:
/// 1. The parent node sents a message indicating the TEM should switch to depthFirst traversal.
/// 2. The number of buildRequests is larger than the batchRequestSize.
/// In both of these cases the system will go from a breadthFirstTraversal to a depthFirst Traversal. In the second case
/// a message will be sent to the parent engine to switch the system to depthFirst traversal as the system is starting to
/// be overloaded with work.
/// In a depth first strategy the buildRequests will be sent to the parent engine one at a time and waiting for results for
/// each buildRequest sent. In a breadthFirst traversal strategy some number of the buildrequests will be sent to the parent engine
/// in a batch of requests. The system will then wait on the results of ALL the build requests sent before continuing
/// to send more build requests.
/// </summary>
private void WaitForBuildResults(int handleId, BuildResult[] buildResultsLocal, BuildRequest[] buildRequests)
{
// If the traversal strategy is breadth first and the number of requests is less than the batchRequestSize
// or if there is only 1 build request then send ALL build requests to the parent engine and wait on the results.
if ((breadthFirstTraversal && buildRequests.Length < batchRequestSize) || buildRequests.Length == 1)
{
engineCallback.PostBuildRequestsToHost(buildRequests);
workerThread.WaitForResults(handleId, buildResultsLocal, buildRequests);
}
else
{
int currentRequestIndex = 0; // Which build request is being processed
// Arrays that will be used to partion the buildRequests array when sending batches of builds requests at a time.
BuildRequest[] wrapperArrayBreadthFirst = new BuildRequest[batchRequestSize];
BuildResult[] resultsArrayBreadthFirst = new BuildResult[batchRequestSize];
// Pre allocate these arrays as they will always be only one element in size. They are assigned to and filled when doing a depth first traversal.
BuildRequest[] wrapperArrayDepthFirst = new BuildRequest[1];
BuildResult[] resultsArrayDepthFirst = new BuildResult[1];
// While there are still requests to send
while (currentRequestIndex < buildRequests.Length)
{
// If there is a breadth first traversal and there are more than batchRequestSize build requests, send the first batchRequestSize, then do the rest depth first
if (breadthFirstTraversal)
{
// Figure out how many requests to send, either the full batch size or only part of a batch
int numberOfRequestsToSend = (buildRequests.Length - currentRequestIndex) < batchRequestSize ? (buildRequests.Length - currentRequestIndex) : batchRequestSize;
// Initialize the wrapper array to how many requests are going to be sent
if (numberOfRequestsToSend != wrapperArrayBreadthFirst.Length)
{
wrapperArrayBreadthFirst = new BuildRequest[numberOfRequestsToSend];
resultsArrayBreadthFirst = new BuildResult[numberOfRequestsToSend];
}
// Fill the wrapper array with one batch of build requests
for (int i = 0; i < numberOfRequestsToSend; i++)
{
wrapperArrayBreadthFirst[i] = buildRequests[currentRequestIndex + i];
wrapperArrayBreadthFirst[i].RequestId = i;
resultsArrayBreadthFirst[i] = null;
}
engineCallback.PostBuildRequestsToHost(wrapperArrayBreadthFirst);
// Only switch from breadth to depth if there are more thanbatchRequestSize items
if ((buildRequests.Length - currentRequestIndex) > batchRequestSize)
{
engineCallback.PostStatus(nodeId, new NodeStatus(false /* use depth first traversal*/), false /* don't block waiting on the send */);
breadthFirstTraversal = false;
}
workerThread.WaitForResults(handleId, resultsArrayBreadthFirst, wrapperArrayBreadthFirst);
Array.Copy(resultsArrayBreadthFirst, 0, buildResultsLocal, currentRequestIndex, numberOfRequestsToSend);
currentRequestIndex += numberOfRequestsToSend;
}
// Proceed with depth first traversal
while ((currentRequestIndex < buildRequests.Length) && !breadthFirstTraversal)
{
wrapperArrayDepthFirst[0] = buildRequests[currentRequestIndex];
buildRequests[currentRequestIndex].RequestId = 0;
resultsArrayDepthFirst[0] = null;
engineCallback.PostBuildRequestsToHost(wrapperArrayDepthFirst);
workerThread.WaitForResults(handleId, resultsArrayDepthFirst, wrapperArrayDepthFirst);
//Copy the result from an intermediate array to the full array
buildResultsLocal[currentRequestIndex] = resultsArrayDepthFirst[0];
//Check if the call failed (null result was returned)
if (buildResultsLocal[currentRequestIndex] == null)
{
return;
}
//Move to the next request
currentRequestIndex++;
}
}
}
}
/// <summary>
/// Call into the engine to figure out the line and column number of the task XML node in the original
/// project context
/// </summary>
internal virtual void GetLineColumnOfXmlNode(int handleId, out int lineNumber, out int columnNumber)
{
engineCallback.GetLineColumnOfXmlNode(handleId, out lineNumber, out columnNumber);
}
/// <summary>
/// Gets the global tasks registry defined by the *.tasks files.
/// </summary>
/// <returns>Global/default tasks registry.</returns>
internal ITaskRegistry GetDefaultTasksRegistry(int handleId)
{
return engineCallback.GetEngineTaskRegistry(handleId);
}
/// <summary>
/// Gets the tasks registry for the given project.
/// </summary>
/// <returns>Project task registry.</returns>
internal ITaskRegistry GetProjectTasksRegistry(int handleId)
{
return engineCallback.GetProjectTaskRegistry(handleId);
}
/// <summary>
/// Gets the path to the tools used for the particular task
/// </summary>
internal string GetToolsPath(int handleId)
{
return engineCallback.GetToolsPath(handleId);
}
internal bool RethrowTaskExceptions()
{
return moduleMode == TaskExecutionModuleMode.SingleProcMode;
}
#endregion
#region Methods called from the engine
/// <summary>
/// Called to execute a task within a target. This method instantiates the task, sets its parameters,
/// and executes it.
/// </summary>
/// <param name="taskState"></param>
public void ExecuteTask(TaskExecutionState taskState)
{
// Fill out the local fields of the task state
taskState.ParentModule = this;
taskState.LoggingService = engineCallback.GetParentEngine().LoggingServices;
taskState.ProfileExecution = profileExecution;
// If we running in single proc mode, we should execute this task on the current thread
if (moduleMode == TaskExecutionModuleMode.SingleProcMode)
{
taskState.ExecuteTask();
}
else
{
// In multiproc mode post the work item to the workerThread queue so it can be executed by the worker thread.
workerThread.PostWorkItem(taskState);
}
}
/// <summary>
/// Uses the parent engine to get the next unique TaskID.
/// </summary>
/// <returns></returns>
internal int GetNextTaskId()
{
return engineCallback.GetParentEngine().GetNextTaskId();
}
/// <summary>
/// This method lets the engine provide node with results of an evaluation it was waiting on.
/// </summary>
/// <param name="buildResult"></param>
internal void PostBuildResults(BuildResult buildResult)
{
// Do nothing in the single proc mode
if (moduleMode == TaskExecutionModuleMode.SingleProcMode)
{
return;
}
buildResult.ConvertToTaskItems();
workerThread.PostBuildResult(buildResult);
}
/// <summary>
/// This function returns the last time TEM was active executing a task
/// </summary>
internal long LastTaskActivity()
{
if (moduleMode != TaskExecutionModuleMode.SingleProcMode)
{
if (activeThreadCount == 0 && workerThread.WorkItemCount == 0)
{
return lastTaskActivity;
}
else
{
return DateTime.Now.Ticks;
}
}
return DateTime.Now.Ticks;
}
internal int[] GetWaitingTaskData(List<BuildRequest[]> outstandingRequests)
{
if (moduleMode != TaskExecutionModuleMode.SingleProcMode)
{
return workerThread.GetWaitingTasksData(outstandingRequests);
}
return new int[0];
}
internal void Shutdown()
{
if (moduleMode != TaskExecutionModuleMode.SingleProcMode)
{
workerThread.Shutdown();
while (overallThreadCount != 0)
{
threadOverallCountEvent.WaitOne();
threadOverallCountEvent.Reset();
}
if (profileExecution)
{
int taskTimeMs = 0;
if (totalTaskTime != 0)
{
TimeSpan taskTimeSpan = new TimeSpan(totalTaskTime);
taskTimeMs = (int)taskTimeSpan.TotalMilliseconds;
}
Console.WriteLine("Node time spent " + taskTimeMs);
}
}
}
#endregion
#region Methods used by worker threads
internal void IncrementOverallThreadCount()
{
Interlocked.Increment(ref overallThreadCount);
}
internal void DecrementOverallThreadCount()
{
Interlocked.Decrement(ref overallThreadCount);
threadOverallCountEvent.Set();
}
internal void DecrementActiveThreadCount()
{
Interlocked.Decrement(ref activeThreadCount);
threadActiveCountEvent.Set();
}
internal void WaitForZeroActiveThreadCount()
{
while (Interlocked.CompareExchange(ref activeThreadCount, 1, 0) != 0)
{
threadActiveCountEvent.WaitOne();
threadActiveCountEvent.Reset();
}
lastTaskActivity = DateTime.Now.Ticks;
}
#endregion
#region Methods used for unittest only
/// <summary>
/// ONLY for unit testing
/// </summary>
internal TaskExecutionModuleMode GetExecutionModuleMode()
{
// The Execution module mode is used to determine if they system is running under single proc or multiproc for the purposes of creating a new thread
// to execute tasks on.
return moduleMode;
}
/// <summary>
/// ONLY for unit testing
/// </summary>
internal TaskWorkerThread GetWorkerThread()
{
return workerThread;
}
#endregion
#region Member data
/// <summary>
/// Callback interface to communicate with the engine
/// </summary>
private EngineCallback engineCallback;
/// <summary>
/// The mode in which the TEM is running
/// </summary>
private TaskExecutionModuleMode moduleMode;
/// <summary>
/// The class used to execute user tasks.
/// </summary>
private TaskWorkerThread workerThread;
// Data shared between all worker threads within the TEM
/// <summary>
/// Total count of worker threads both active and inactive
/// </summary>
private int overallThreadCount;
/// <summary>
/// Event indicated a decrease in overallThreadCount due to an exit of a thread
/// </summary>
private ManualResetEvent threadOverallCountEvent;
/// <summary>
/// Count of active thread (i.e. threads in user code). Has to be either 0 or 1
/// </summary>
private int activeThreadCount;
/// <summary>
/// Event indicating a decrease in activeThreadCount due to a thread leaving user code
/// </summary>
private ManualResetEvent threadActiveCountEvent;
/// <summary>
/// Time stamp of last execution of user code. Only valid if activeThreadCount == 0.
/// </summary>
private long lastTaskActivity;
/// <summary>
/// Specifies the traversal type for callbacks. If true multiple build requests will be sent
/// to the engine if false the requests will be sent one at a time.
/// </summary>
private bool breadthFirstTraversal;
/// <summary>
/// Specifies if the timing data on task execution should be collected
/// </summary>
private bool profileExecution;
/// <summary>
/// Total time spent executing task code. Only valid if profileExecution is true.
/// </summary>
private long totalTaskTime;
/// <summary>
/// This property allows a task to query whether or not the system is running in single process mode or multi process mode.
/// Single process mode (IsRunningMultipleNodes = false) is where the engine is initialized with the number of cpus = 1 and the engine is not a child engine.
/// The engine is in multi process mode (IsRunningMultipleNodes = true) when the engine is initialized with a number of cpus > 1 or the engine is a child engine.
/// </summary>
private bool isRunningMultipleNodes;
private static StringBuilder currentDirectoryBuffer = new StringBuilder(270);
/// <summary>
/// In a multiproc build this is the maximum number of build requests which will be sent at a time to the parent engine
/// A default of 10 was an arbitrary number but turned out to be a good balance between being too small
/// causing the system to run out of work too quickly and being too big and flooding the system with requests.
/// </summary>
private const int defaultBatchRequestSize = 10;
private int batchRequestSize = defaultBatchRequestSize;
/// <summary>
/// The nodeId of the node the TaskExecutionModule is running on
/// </summary>
private int nodeId = -1;
#endregion
#region Enums
internal enum TaskExecutionModuleMode
{
/// <summary>
/// In this mode the tasks should be executed on the calling thread
/// </summary>
SingleProcMode = 0,
/// <summary>
/// In this mode the tasks should be executed on a different thread and the execute calls
/// should return immediately. The messages due to the task are not flushed.
/// </summary>
MultiProcFullNodeMode = 1,
}
#endregion
}
}
|