diff --git a/src/System.Management.Automation/engine/ExternalScriptInfo.cs b/src/System.Management.Automation/engine/ExternalScriptInfo.cs index 642eb4d8fe9..9f817bb8bdb 100644 --- a/src/System.Management.Automation/engine/ExternalScriptInfo.cs +++ b/src/System.Management.Automation/engine/ExternalScriptInfo.cs @@ -188,7 +188,10 @@ public override SessionStateEntryVisibility Visibility { get { - if (Context == null) return SessionStateEntryVisibility.Public; + if (Context == null) + { + return SessionStateEntryVisibility.Public; + } return Context.EngineSessionState.CheckScriptVisibility(_path); } @@ -267,7 +270,11 @@ internal ScriptBlockAst GetScriptBlockAst() var scriptContents = ScriptContents; if (_scriptBlock == null) { - this.ScriptBlock = ScriptBlock.TryGetCachedScriptBlock(_path, scriptContents); + CompiledScriptBlockData compiledScriptBlockData = ScriptBlock.TryGetCachedCompiledScriptBlock(_path, scriptContents); + if (compiledScriptBlockData != null) + { + this.ScriptBlock = new ScriptBlock(compiledScriptBlockData); + } } if (_scriptBlock != null) @@ -305,7 +312,7 @@ internal ScriptBlockAst GetScriptBlockAst() if (errors.Length == 0) { this.ScriptBlock = new ScriptBlock(_scriptBlockAst, isFilter: false); - ScriptBlock.CacheScriptBlock(_scriptBlock.Clone(), _path, scriptContents); + ScriptBlock.CacheCompiledScriptBlock(_scriptBlock.Clone(), _path, scriptContents); } } diff --git a/src/System.Management.Automation/engine/InitialSessionState.cs b/src/System.Management.Automation/engine/InitialSessionState.cs index bfae1b3e6b4..950397fc301 100644 --- a/src/System.Management.Automation/engine/InitialSessionState.cs +++ b/src/System.Management.Automation/engine/InitialSessionState.cs @@ -5269,6 +5269,7 @@ private static void InitializeCoreCmdletsAndProviders( { "Import-Module", new SessionStateCmdletEntry("Import-Module", typeof(ImportModuleCommand), helpFile) }, { "Invoke-Command", new SessionStateCmdletEntry("Invoke-Command", typeof(InvokeCommandCommand), helpFile) }, { "Invoke-History", new SessionStateCmdletEntry("Invoke-History", typeof(InvokeHistoryCommand), helpFile) }, + { "Measure-Script", new SessionStateCmdletEntry("Measure-Script", typeof(MeasureScriptCommand), helpFile) }, { "New-Module", new SessionStateCmdletEntry("New-Module", typeof(NewModuleCommand), helpFile) }, { "New-ModuleManifest", new SessionStateCmdletEntry("New-ModuleManifest", typeof(NewModuleManifestCommand), helpFile) }, { "New-PSRoleCapabilityFile", new SessionStateCmdletEntry("New-PSRoleCapabilityFile", typeof(NewPSRoleCapabilityFileCommand), helpFile) }, diff --git a/src/System.Management.Automation/engine/debugger/debugger.cs b/src/System.Management.Automation/engine/debugger/debugger.cs index e8e8c849442..418d1d4d7e5 100644 --- a/src/System.Management.Automation/engine/debugger/debugger.cs +++ b/src/System.Management.Automation/engine/debugger/debugger.cs @@ -1135,6 +1135,16 @@ internal void EnterScriptFunction(FunctionContext functionContext) Diagnostics.Assert(functionContext._executionContext == _context, "Wrong debugger is being used."); var invocationInfo = (InvocationInfo)functionContext._localsTuple.GetAutomaticVariable(AutomaticVariable.MyInvocation); + + if (ProfilerEventSource.LogInstance.IsEnabled()) + { + ProfilerEventSource.LogInstance.SequencePoint( + functionContext._scriptBlock?.Id ?? Guid.Empty, + functionContext._executionContext.CurrentRunspace.InstanceId, + _callStack.Last()?.FunctionContext._scriptBlock.Id ?? Guid.Empty, + 0); + } + var newCallStackInfo = new CallStackInfo { InvocationInfo = invocationInfo, @@ -1232,6 +1242,25 @@ internal void RegisterScriptFile(string path, string scriptContents) } } + internal Guid GetParentScriptBlockId(int sequencePointPosition) + { + // Sequence point on 0 position is an entry point in a scriptblock. + // In the time the callstack has still point to parent scriptblock + // so we take last element from the callstack, + // for rest sequence points we take an element before last as parent. + int shift = sequencePointPosition == 0 ? 1 : 2; + if (_callStack.Count - shift >= 0) + { + ScriptBlock scriptBlock = _callStack[_callStack.Count - shift].FunctionContext._scriptBlock; + if (scriptBlock is not null) + { + return scriptBlock.Id; + } + } + + return Guid.Empty; + } + #endregion Call stack management #region setting breakpoints diff --git a/src/System.Management.Automation/engine/interpreter/PowerShellInstructions.cs b/src/System.Management.Automation/engine/interpreter/PowerShellInstructions.cs index 5592af6f526..9f251d7a9ff 100644 --- a/src/System.Management.Automation/engine/interpreter/PowerShellInstructions.cs +++ b/src/System.Management.Automation/engine/interpreter/PowerShellInstructions.cs @@ -29,15 +29,13 @@ private UpdatePositionInstruction(bool checkBreakpoints, int sequencePoint) public override int Run(InterpretedFrame frame) { var functionContext = frame.FunctionContext; - var context = frame.ExecutionContext; - - functionContext._currentSequencePointIndex = _sequencePoint; if (_checkBreakpoints) { - if (context._debuggingMode > 0) - { - context.Debugger.OnSequencePointHit(functionContext); - } + functionContext.UpdatePosition(_sequencePoint); + } + else + { + functionContext.UpdatePositionNoBreak(_sequencePoint); } return +1; diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs index 68bb523427b..3b79ee406d6 100644 --- a/src/System.Management.Automation/engine/parser/Compiler.cs +++ b/src/System.Management.Automation/engine/parser/Compiler.cs @@ -241,6 +241,12 @@ internal static class CachedReflectionInfo internal static readonly MethodInfo FunctionContext_PushTrapHandlers = typeof(FunctionContext).GetMethod(nameof(FunctionContext.PushTrapHandlers), InstanceFlags); + internal static readonly MethodInfo FunctionContext_UpdatePosition = + typeof(FunctionContext).GetMethod(nameof(FunctionContext.UpdatePosition), InstanceFlags); + + internal static readonly MethodInfo FunctionContext_UpdatePositionNoBreak = + typeof(FunctionContext).GetMethod(nameof(FunctionContext.UpdatePositionNoBreak), InstanceFlags); + internal static readonly MethodInfo FunctionOps_DefineFunction = typeof(FunctionOps).GetMethod(nameof(FunctionOps.DefineFunction), StaticFlags); @@ -802,6 +808,30 @@ internal void PopTrapHandlers() { _traps.RemoveAt(_traps.Count - 1); } + + internal void UpdatePositionNoBreak(int pos) + { + _currentSequencePointIndex = pos; + } + + internal void UpdatePosition(int pos) + { + UpdatePositionNoBreak(pos); + + if (ProfilerEventSource.LogInstance.IsEnabled()) + { + ProfilerEventSource.LogInstance.SequencePoint( + _scriptBlock?.Id ?? Guid.Empty, + _executionContext.CurrentRunspace.InstanceId, + _executionContext.Debugger.GetParentScriptBlockId(pos), + pos); + } + + if (_executionContext._debuggingMode > 0) + { + _executionContext.Debugger.OnSequencePointHit(this); + } + } } internal class Compiler : ICustomAstVisitor2 @@ -2145,13 +2175,28 @@ private static object GetExpressionValue( var pipe = new Pipe(resultList); try { + ScriptBlock scriptBlock = null; + if (ProfilerEventSource.LogInstance.IsEnabled()) + { + // We need a scriptblock Id to keep track of the call stack. + scriptBlock = ScriptBlock.Create(context, expressionAst.Extent.Text); + + // No need to optimize - we only want to add to the scriptblock cache + // so that we can get rundown events. + // We can safely do this because lambda and sequencePoints + // parameters of the method come always from callsites as null. + // (We could remove them at all.) + scriptBlock.Compile(optimized: false); + } + var functionContext = new FunctionContext { _sequencePoints = sequencePoints, _executionContext = context, _file = expressionAst.Extent.File, _outputPipe = pipe, - _localsTuple = MutableTuple.MakeTuple(localsTupleType, DottedLocalsNameIndexMap) + _localsTuple = MutableTuple.MakeTuple(localsTupleType, DottedLocalsNameIndexMap), + _scriptBlock = scriptBlock }; if (usingValues != null) { @@ -7055,26 +7100,10 @@ public override Expression Reduce() exprs.Add(Expression.DebugInfo(_debugSymbolDocument, _extent.StartLineNumber, _extent.StartColumnNumber, _extent.EndLineNumber, _extent.EndColumnNumber)); } - exprs.Add( - Expression.Assign( - Expression.Field(Compiler.s_functionContext, CachedReflectionInfo.FunctionContext__currentSequencePointIndex), - ExpressionCache.Constant(_sequencePoint))); - - if (_checkBreakpoints) - { - exprs.Add( - Expression.IfThen( - Expression.GreaterThan( - Expression.Field(Compiler.s_executionContextParameter, CachedReflectionInfo.ExecutionContext_DebuggingMode), - ExpressionCache.Constant(0)), - Expression.Call( - Expression.Field(Compiler.s_executionContextParameter, CachedReflectionInfo.ExecutionContext_Debugger), - CachedReflectionInfo.Debugger_OnSequencePointHit, - Compiler.s_functionContext))); - } - - exprs.Add(ExpressionCache.Empty); - + MethodInfo method = _checkBreakpoints + ? CachedReflectionInfo.FunctionContext_UpdatePosition + : CachedReflectionInfo.FunctionContext_UpdatePositionNoBreak; + exprs.Add(Expression.Call(Compiler.s_functionContext, method, ExpressionCache.Constant(_sequencePoint))); return Expression.Block(exprs); } diff --git a/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs b/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs index 79881796cad..adbeefd293c 100644 --- a/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs +++ b/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs @@ -39,18 +39,46 @@ internal enum ScriptBlockClauseToInvoke internal class CompiledScriptBlockData { + private static readonly ConcurrentDictionary s_compiledScriptBlockTable + = new ConcurrentDictionary(); + + internal static void ClearCompiledScriptBlockTable() + { + s_compiledScriptBlockTable.Clear(); + } + + internal static ICollection GetCompiledScriptBlockList() + { + return s_compiledScriptBlockTable.Values; + } + + internal static void InitCompiledScriptBlockTable() + { + ClearCompiledScriptBlockTable(); + + foreach (CompiledScriptBlockData csb in ScriptBlock.GetCachedCompiledScriptBlockData()) + { + s_compiledScriptBlockTable.TryAdd(csb, csb); + } + } + + internal void RegisterCompiledScriptBlockData() + { + s_compiledScriptBlockTable.TryAdd(this, this); + } + internal CompiledScriptBlockData(IParameterMetadataProvider ast, bool isFilter) { _ast = ast; - this.IsFilter = isFilter; - this.Id = Guid.NewGuid(); + IsFilter = isFilter; + Id = Guid.NewGuid(); } internal CompiledScriptBlockData(string scriptText, bool isProductCode) { _isProductCode = isProductCode; _scriptText = scriptText; - this.Id = Guid.NewGuid(); + Id = Guid.NewGuid(); } internal bool Compile(bool optimized) @@ -78,6 +106,11 @@ internal bool Compile(bool optimized) CompileOptimized(); } + if (ProfilerEventSource.LogInstance.IsEnabled()) + { + RegisterCompiledScriptBlockData(); + } + return optimized; } @@ -131,6 +164,11 @@ private void InitializeMetadata() private void CompileUnoptimized() { + if (_compiledUnoptimized) + { + return; + } + lock (this) { if (_compiledUnoptimized) @@ -146,6 +184,11 @@ private void CompileUnoptimized() private void CompileOptimized() { + if (_compiledOptimized) + { + return; + } + lock (this) { if (_compiledOptimized) @@ -548,7 +591,7 @@ internal ScriptBlock(IParameterMetadataProvider ast, bool isFilter) { } - private ScriptBlock(CompiledScriptBlockData scriptBlockData) + internal ScriptBlock(CompiledScriptBlockData scriptBlockData) { _scriptBlockData = scriptBlockData; @@ -576,24 +619,25 @@ protected ScriptBlock(SerializationInfo info, StreamingContext context) { } - private static readonly ConcurrentDictionary, ScriptBlock> s_cachedScripts = - new ConcurrentDictionary, ScriptBlock>(); + private static readonly ConcurrentDictionary, CompiledScriptBlockData> s_cachedScripts = + new ConcurrentDictionary, CompiledScriptBlockData>(); + + internal static ICollection GetCachedCompiledScriptBlockData() + { + return s_cachedScripts.Values; + } - internal static ScriptBlock TryGetCachedScriptBlock(string fileName, string fileContents) + internal static CompiledScriptBlockData TryGetCachedCompiledScriptBlock(string fileName, string fileContents) { if (InternalTestHooks.IgnoreScriptBlockCache) { return null; } - ScriptBlock scriptBlock; var key = Tuple.Create(fileName, fileContents); - if (s_cachedScripts.TryGetValue(key, out scriptBlock)) + if (s_cachedScripts.TryGetValue(key, out var compiledScriptBlockData)) { - Diagnostics.Assert( - scriptBlock.SessionStateInternal == null, - "A cached scriptblock should not have it's session state bound, that causes a memory leak."); - return scriptBlock.Clone(); + return compiledScriptBlockData; } return null; @@ -605,7 +649,7 @@ private static bool IsDynamicKeyword(Ast ast) private static bool IsUsingTypes(Ast ast) => ast is UsingStatementAst cmdAst && cmdAst.IsUsingModuleOrAssembly(); - internal static void CacheScriptBlock(ScriptBlock scriptBlock, string fileName, string fileContents) + internal static void CacheCompiledScriptBlock(ScriptBlock scriptBlock, string fileName, string fileContents) { if (InternalTestHooks.IgnoreScriptBlockCache) { @@ -632,15 +676,7 @@ internal static void CacheScriptBlock(ScriptBlock scriptBlock, string fileName, } var key = Tuple.Create(fileName, fileContents); - s_cachedScripts.TryAdd(key, scriptBlock); - } - - /// - /// Clears the cached scriptblocks. - /// - internal static void ClearScriptBlockCache() - { - s_cachedScripts.Clear(); + s_cachedScripts.TryAdd(key, scriptBlock.ScriptBlockData); } internal static readonly ScriptBlock EmptyScriptBlock = @@ -648,10 +684,10 @@ internal static void ClearScriptBlockCache() internal static ScriptBlock Create(Parser parser, string fileName, string fileContents) { - var scriptBlock = TryGetCachedScriptBlock(fileName, fileContents); - if (scriptBlock != null) + var compiledScriptBlockData = TryGetCachedCompiledScriptBlock(fileName, fileContents); + if (compiledScriptBlockData != null) { - return scriptBlock; + return new ScriptBlock(compiledScriptBlockData); } var ast = parser.Parse(fileName, fileContents, null, out ParseError[] errors, ParseMode.Default); @@ -661,12 +697,9 @@ internal static ScriptBlock Create(Parser parser, string fileName, string fileCo } var result = new ScriptBlock(ast, isFilter: false); - CacheScriptBlock(result, fileName, fileContents); + CacheCompiledScriptBlock(result, fileName, fileContents); - // The value returned will potentially be bound to a session state. We don't want - // the cached script block to end up being bound to any session state, so clone - // the return value to ensure the cached value has no session state. - return result.Clone(); + return result; } internal ScriptBlock Clone() => new ScriptBlock(_scriptBlockData); diff --git a/src/System.Management.Automation/engine/runtime/Profiler.cs b/src/System.Management.Automation/engine/runtime/Profiler.cs new file mode 100644 index 00000000000..4345ed3ebde --- /dev/null +++ b/src/System.Management.Automation/engine/runtime/Profiler.cs @@ -0,0 +1,526 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Management.Automation.Internal; + +namespace System.Management.Automation +{ + /// + /// This class is a source of performance events for script block profiling. + /// Guid is {092ae15a-d5fb-5d8d-9ffd-68891d24c5f6}. + /// + [EventSource(Name = "Microsoft-PowerShell-Profiler")] + internal class ProfilerEventSource : EventSource + { + internal static ProfilerEventSource LogInstance = new ProfilerEventSource(); + + [Event(1, Opcode = EventOpcode.Start, ActivityOptions = EventActivityOptions.Recursive)] + public void SequencePoint(Guid scriptBlockId, Guid runspaceInstanceId, Guid parentScriptBlockId, int sequencePointPosition) + { + // We could use: + // WriteEvent(eventId: 1, ScriptBlockId, SequencePointPosition); + // but we care about performance. + if (IsEnabled()) + { + unsafe + { + EventData* eventPayload = stackalloc EventData[4]; + + eventPayload[0] = new EventData + { + Size = sizeof(Guid), + DataPointer = (IntPtr)(&scriptBlockId) + }; + eventPayload[1] = new EventData + { + Size = sizeof(Guid), + DataPointer = (IntPtr)(&runspaceInstanceId) + }; + eventPayload[2] = new EventData + { + Size = sizeof(Guid), + DataPointer = (IntPtr)(&parentScriptBlockId) + }; + eventPayload[3] = new EventData + { + Size = sizeof(int), + DataPointer = (IntPtr)(&sequencePointPosition) + }; + + WriteEventCore(eventId: 1, eventDataCount: 4, eventPayload); + } + } + } + + protected override void OnEventCommand(EventCommandEventArgs command) + { + base.OnEventCommand(command); + + if (command.Command == EventCommand.Disable) + { + // At the end of the profile session, we send all the metadata. + ProfilerRundownEventSource.LogInstance.WriteRundownEvents(); + } + } + } + + /// + /// This class is a source of rundown (meta data) events for script block profiling. + /// Guid is {f348266e-dde6-5590-4bc9-10e1e2b6fe16}. + /// + [EventSource(Name = "Microsoft-PowerShell-Profiler-Rundown")] + internal class ProfilerRundownEventSource : EventSource + { + internal static ProfilerRundownEventSource LogInstance = new ProfilerRundownEventSource(); + + [Event(2)] + public void ScriptBlockRundown( + Guid scriptBlockId, + string scriptBlockText) + { + // It is not performance critical so we use the standard method overload. + WriteEvent( + eventId: 2, + scriptBlockId, + scriptBlockText); + } + + [Event(3)] + public void SequencePointRundown( + Guid scriptBlockId, + int sequencePointCount, + int sequencePoint, + string? file, + int startLineNumber, + int startColumnNumber, + int endLineNumber, + int endColumnNumber, + string text, + int startOffset, + int endOffset) + { + // It is not performance critical so we use the standard method overload. + WriteEvent( + eventId: 3, + scriptBlockId, + sequencePointCount, + sequencePoint, + file, + startLineNumber, + startColumnNumber, + endLineNumber, + endColumnNumber, + text, + startOffset, + endOffset); + } + + [NonEvent] + internal void WriteRundownEvents() + { + foreach (var compiledScriptBlock in CompiledScriptBlockData.GetCompiledScriptBlockList()) + { + ScriptBlockRundown(compiledScriptBlock.Id, compiledScriptBlock.Ast.Body.Extent.Text); + + if (compiledScriptBlock.SequencePoints is null) + { + // Why do we get script blocks without sequence points? + // See a comment in Compiler.cs line 2035. + continue; + } + + for (var position = 0; position < compiledScriptBlock.SequencePoints.Length; position++) + { + var sequencePoint = compiledScriptBlock.SequencePoints[position]; + + SequencePointRundown( + compiledScriptBlock.Id, + compiledScriptBlock.SequencePoints.Length, + position, + sequencePoint.File, + sequencePoint.StartLineNumber, + sequencePoint.StartColumnNumber, + sequencePoint.EndLineNumber, + sequencePoint.EndColumnNumber, + sequencePoint.Text, + sequencePoint.StartOffset, + sequencePoint.EndOffset); + } + } + } + + protected override void OnEventCommand(EventCommandEventArgs command) + { + base.OnEventCommand(command); + + if (command.Command == EventCommand.Enable) + { + CompiledScriptBlockData.InitCompiledScriptBlockTable(); + } + else if (command.Command == EventCommand.Disable) + { + CompiledScriptBlockData.ClearCompiledScriptBlockTable(); + } + } + + protected override void Dispose(bool disposing) + { + CompiledScriptBlockData.ClearCompiledScriptBlockTable(); + base.Dispose(disposing); + } + } + + /// + /// Represents a span of text in a script. + /// + internal struct ScriptExtentEventData + { + /// + /// The filename the extent includes, or null if the extent is not included in any file. + /// + public string File; + + /// + /// The line number at the beginning of the extent, with the value 1 being the first line. + /// + public int StartLineNumber; + + /// + /// The column number at the beginning of the extent, with the value 1 being the first column. + /// + public int StartColumnNumber; + + /// + /// The line number at the end of the extent, with the value 1 being the first line. + /// + public int EndLineNumber; + + /// + /// The column number at the end of the extent, with the value 1 being the first column. + /// + public int EndColumnNumber; + + /// + /// The script text that the extent includes. + /// + public string Text; + + /// + /// The starting offset of the extent. + /// + public int StartOffset; + + /// + /// The ending offset of the extent. + /// + public int EndOffset; + } + + /// + /// This class is PowerShell script block profiler. + /// + internal class InternalProfiler : EventListener + { + /// + /// Represents a SequencePoint profile event data. + /// The event is raised at every sequence point start. + /// The event must be as small as possible for performance. + /// + internal struct SequencePointProfileEventData + { + /// + /// Start time of the SequencePoint. + /// + public DateTime Timestamp; + + /// + /// Unique identifer of the script block. + /// + public Guid ScriptId; + + /// + /// Unique identifer of the runspace. + /// + public Guid RunspaceId; + + /// + /// Unique identifer of the parent script block. + /// + public Guid ParentScriptBlockId; + + /// + /// SequencePoint index number/position of the script block. + /// + public int SequencePointPosition; + } + + /// + /// Represents a SequencePoint rundown profile event data. + /// The rundown event contains a meta data about script block sequence points. + /// The rundown event is raised once for every script block sequence point + /// at profile session end. + /// + internal struct CompiledScriptBlockRundownProfileEventData + { + /// + /// Timestamp of first rundown profile event for the script block. + /// + public DateTime Timestamp; + + /// + /// Unique identifer of the script block. + /// + public Guid ScriptId; + + /// + /// Sequence points of the script block. + /// + public ScriptExtentEventData[] SequencePoints; + } + + // Buffer to collect a performance event data. + internal List SequencePointProfileEvents = new List(5000); + + // Buffer to collect a script block meta data. + internal Dictionary CompiledScriptBlockMetaData = new Dictionary(5000); + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + var payload = eventData.Payload; + if (payload is null) + { + // there is a bug in our custom EventSource. + throw new ArgumentNullException(nameof(payload)); + } + + switch (eventData.EventId) + { + case 1: + // Performance event + SequencePointProfileEvents.Add(new SequencePointProfileEventData + { + Timestamp = eventData.TimeStamp, + ScriptId = (Guid)payload[0]!, + RunspaceId = (Guid)payload[1]!, + ParentScriptBlockId = (Guid)payload[2]!, + SequencePointPosition = (int)payload[3]! + }); + break; + case 2: + // Scriptblock rundown event + break; + case 3: + // SequencePoint rundown event + ScriptExtentEventData sequencePoint; + var scriptId = (Guid)payload[0]!; + var sequencePointCount = (int)payload[1]!; + var pos = (int)payload[2]!; + sequencePoint.File = (string)payload[3]!; + sequencePoint.StartLineNumber = (int)payload[4]!; + sequencePoint.StartColumnNumber = (int)payload[5]!; + sequencePoint.EndLineNumber = (int)payload[6]!; + sequencePoint.EndColumnNumber = (int)payload[7]!; + sequencePoint.Text = (string)payload[8]!; + sequencePoint.StartOffset = (int)payload[9]!; + sequencePoint.EndOffset = (int)payload[10]!; + + if (CompiledScriptBlockMetaData.TryGetValue(scriptId, out var sbe)) + { + sbe.SequencePoints[pos] = sequencePoint; + } + else + { + sbe = new CompiledScriptBlockRundownProfileEventData() + { + Timestamp = eventData.TimeStamp, + ScriptId = scriptId, + SequencePoints = new ScriptExtentEventData[sequencePointCount] + }; + + sbe.SequencePoints[pos] = sequencePoint; + + CompiledScriptBlockMetaData.TryAdd(sbe.ScriptId, sbe); + } + + break; + } + } + + /// + /// Start the profiler. + /// + public void EnableEvents() + { + EnableEvents(ProfilerRundownEventSource.LogInstance, EventLevel.LogAlways); + EnableEvents(ProfilerEventSource.LogInstance, EventLevel.LogAlways); + } + + /// + /// Stop the profiler. + /// + public void DisableEvents() + { + DisableEvents(ProfilerEventSource.LogInstance); + DisableEvents(ProfilerRundownEventSource.LogInstance); + } + } + + /// + /// The cmdlet profiles a script block. + /// + [Cmdlet(VerbsDiagnostic.Measure, "Script", HelpUri = "https://go.microsoft.com/fwlink/?LinkId=", RemotingCapability = RemotingCapability.None)] + [OutputType(typeof(ProfileEventRecord))] + public class MeasureScriptCommand : PSCmdlet + { + /// + /// Gets or sets a script block to profile. + /// + [Parameter(Position = 0, Mandatory = true)] + public ScriptBlock ScriptBlock { get; set; } = null!; + + /// + /// Process a profile data. + /// + protected override void EndProcessing() + { + using (var profiler = new InternalProfiler()) + { + try + { + profiler.EnableEvents(); + + ScriptBlock.InvokeWithPipe( + useLocalScope: false, + errorHandlingBehavior: ScriptBlock.ErrorHandlingBehavior.WriteToCurrentErrorPipe, + dollarUnder: null, + input: Array.Empty(), + scriptThis: AutomationNull.Value, + outputPipe: new Pipe { NullPipe = true }, + invocationInfo: null); + } + finally + { + profiler.DisableEvents(); + } + + var events = profiler.SequencePointProfileEvents; + if (events.Count == 0) + { + return; + } + + var metaData = profiler.CompiledScriptBlockMetaData; + + // 1. We have only start timestamp for a sequence point. + // To evaluate a duration of the sequence point + // we take a start timestamp from next sequence point + // that is actually a stop timestamp for the previous sequence point. + // + // 2. We handle events separately for each runspace. + // + // 3. Last event of every runspace we output as-is + // without evaluating a duration because we have not a stop event. + // --------------------------------------------------------------- + Dictionary runspaceCurrentEvent = new(); + + for (var i = 0; i < events.Count; i++) + { + var nextEvent = events[i]; + + if (runspaceCurrentEvent.TryGetValue(nextEvent.RunspaceId, out var currentEvent)) + { + runspaceCurrentEvent[nextEvent.RunspaceId] = nextEvent; + } + else + { + // It is first event in the runspace. + runspaceCurrentEvent.Add(nextEvent.RunspaceId, nextEvent); + continue; + } + + if (metaData.TryGetValue(currentEvent.ScriptId, out var compiledScriptBlockData)) + { + var extent = compiledScriptBlockData.SequencePoints[currentEvent.SequencePointPosition]; + var result = new ProfileEventRecord + { + StartTime = currentEvent.Timestamp.TimeOfDay, + Duration = nextEvent.Timestamp - currentEvent.Timestamp, + Source = extent.Text, + Extent = extent, + RunspaceId = currentEvent.RunspaceId, + ParentScriptBlockId = currentEvent.ParentScriptBlockId, + ScriptBlockId = currentEvent.ScriptId + }; + + WriteObject(result); + } + } + + foreach (var currentEvent in runspaceCurrentEvent.Values) + { + if (metaData.TryGetValue(currentEvent.ScriptId, out var compiledScriptBlockData)) + { + var extent = compiledScriptBlockData.SequencePoints[currentEvent.SequencePointPosition]; + var result = new ProfileEventRecord + { + StartTime = currentEvent.Timestamp.TimeOfDay, + Duration = TimeSpan.Zero, + Source = extent.Text, + Extent = extent, + RunspaceId = currentEvent.RunspaceId, + ParentScriptBlockId = currentEvent.ParentScriptBlockId, + ScriptBlockId = currentEvent.ScriptId + }; + + WriteObject(result); + } + } + } + } + + /// + /// Measure-ScriptBlock output type. + /// + internal struct ProfileEventRecord + { + /// + /// StartTime of event. + /// + public TimeSpan StartTime; + + /// + /// Duration of event. + /// + public TimeSpan Duration; + + /// + /// Script text. + /// + public string Source; + + /// + /// Script Extent. + /// + public ScriptExtentEventData Extent; + + /// + /// Unique identifer of the runspace. + /// + public Guid RunspaceId; + + /// + /// Unique identifer of the parent script block. + /// + public Guid ParentScriptBlockId; + + /// + /// Unique identifer of the script block. + /// + public Guid ScriptBlockId; + } + } +} diff --git a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 index e970d5a06a8..8950c958e4f 100644 --- a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 +++ b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 @@ -357,6 +357,7 @@ Describe "Verify approved aliases list" -Tags "CI" { "Cmdlet", "Limit-EventLog", "", $($FullCLR ), "", "", "" "Cmdlet", "Measure-Command", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None" "Cmdlet", "Measure-Object", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None" +"Cmdlet", "Measure-Script", "", $( $CoreWindows -or $CoreUnix), "", "", "None" "Cmdlet", "Move-Item", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "Medium" "Cmdlet", "Move-ItemProperty", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "Medium" "Cmdlet", "New-Alias", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "Low" diff --git a/test/powershell/engine/Help/HelpSystem.Tests.ps1 b/test/powershell/engine/Help/HelpSystem.Tests.ps1 index bd523b1033e..3aa189fae81 100644 --- a/test/powershell/engine/Help/HelpSystem.Tests.ps1 +++ b/test/powershell/engine/Help/HelpSystem.Tests.ps1 @@ -17,7 +17,8 @@ $script:cmdletsToSkip = @( "Enable-ExperimentalFeature", "Disable-ExperimentalFeature", "Get-PSSubsystem", - "Switch-Process" + "Switch-Process", + "Measure-Script" ) function UpdateHelpFromLocalContentPath {