-
Notifications
You must be signed in to change notification settings - Fork 8.1k
PowerShell profiler #13673
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PowerShell profiler #13673
Conversation
|
Looks good. I am not able to build it on Windows. Cleaned up and ran bootstrap script, but I still see |
|
@lzybkr I added a scriptblock rundown event per #13589 (comment)
Also I found how use ConditionalWeakTable. With it I get strange scriptblocks without sequence points. @nohwnd It is very strange. It seems your local fork is broken. At least you could do |
lzybkr
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks pretty good, I especially like relying on the rundown event internally which means it should work well out of process as well.
|
@IISResetMe (who adapted my SDK sample into the module PSProfiler) might also want to take a look. |
|
What kind of support in PerfView are you looking for? |
|
The output format is slightly awkward, but beyond that I could definitely work with this: Will take a closer look at the code tomorrow |
|
@brianrob - I think PowerShell maintainers and some module authors would appreciate hybrid PowerShell/C# call stacks from CPU sampling. This PR is generating minimal events (analogous to an IP but no stack) while profiling via an EventSource (using instrumentation in the generated code which is interpreted and lazily jit compiled). After turning off profiling, a rundown event provider is generating metadata to recover the source line and function details from the minimal events. @iSazonov may have other ideas how PowerShell users might use perfview or wpa. |
My thoughts is very simple. Since users will can able to collect PowerShell performance events out-of-process they need tools to analyze the data.
@lzybkr Could you please point me where we could issue a stop event for scriptblock/sequence point? @brianrob What do you think? Is it possible? Could you tell me how we should create events correctly for PerfView? |
|
Tried running this over a Pester test and it failed. Here is a mini repro: _scriptBlock is null when you hit the |
|
@nohwnd for reference your original error was because the resource strings needed to be rebuilt. |
|
I just skimmed the PR to try and understand how things work. I would like to make sure I understand how things work, and then have a few questions. My understanding is that there is a desire to enable profiling in two ways:
There are a couple of pieces of functionality that I can see being implemented in PerfView. Of course, I'm open to feedback and other ideas as well:
For the computing resource based views, we'd need to understand a couple of things:
Right now, PerfView has functionality that does this for jitted code that is compiled on-the-fly by the .NET runtime. This is done by using rundown events to provide function memory ranges and symbols. There's mention of both interpreted and jitted code here. For interpreted code, we'd need events that would allow us to build the appropriate stack. You could imagine doing this with start/stop events that wrap the "scope" of each frame. This would work as long as collection started before the invocation. If it is started in the middle of an invocation, we'd see broken stacks, but these may still be valuable depending on how much movement up and down the stack there is. For jitted code, are we talking about using the .NET runtime's JIT, or something else? |
Yes.
Yes for 1. Currently we issue an event with number 1 as start event but without the "start" label. For 2. I am not sure we can utilize resource tracking because scripts are executed in runspaces (you can think that it is special thread) and there could be some parallel runspaces so we cannot directly know which runspace is consuming resources- we should track a long correlation sequence scriptblock-runspace-.Net thread. It looks too complex for me. Although it is possible... It is my answer on "For jitted code, are we talking about using the .NET runtime's JIT, or something else?" too - it would interesting to have the correlated views (script - JIT) but looks too complex for the first step PR. |
NRE is in "ProfilerEventSource.LogInstance.SequencePoint(_scriptBlock.Id, pos);" (in UpdatePositionNoBreak()) because "_scriptBlock" is null. I wonder why... It seems a "bug" mentioned above. |
|
@brianrob PowerShell does use the .NET jit via LINQ expression trees. PowerShell forked the DLR to use the interpreter which will jit compile code if it executes enough times. |
We catch NRE on "@()". It is parameter binding. This is evaluated out of current scripblock. There is created pseudo functioncontext with sequence points but without a reference to current scriptblock.
Since these code paths is complex, with static methods without scriptblock parameter it is a huge work to fix all them. |
a3829cc to
669996a
Compare
|
I just tried it out with some real scripts and my module that summarizes the results and it works nicely. I am preparing a presentation about this for PWSH24. Is it reasonable to expect that this will be available in a month or two? (even if behind an experimental flag) |
|
@nohwnd Thanks! Do you ask about 7.1 or 7.2? I am sure we will get this in 7.2 Preview1 as experimental. |
|
7.2 is okay. I just wanted to know when roughly this will be ready to try, because I have only a version that works with PowerShell 5, by doing some "reflection" magic, but nothing for the PowerShell core versions. |
|
@nohwnd .Net 5.0 will be released in November. PowerShell 7.1 will be released in the time too. Then the holidays come. I'd expect 7.2 Preview1 in January unless MSFT team forked earlier. |
|
I agree with @nohwnd that the cmdlet should be |
| /// <summary> | ||
| /// Measure-ScriptBlock output type. | ||
| /// </summary> | ||
| internal struct ProfileEventRecord |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be public as it's a type returned to the user as output
|
Was playing with this branch and made a function to consolidate and sort the results https://gist.github.com/SteveL-MSFT/f602dd7e3faac36cc8c781b3c07d036e.
|
First of all, we need answers to the basic questions that have been formulated. I saw this PR as more of a problem study than a final product. |
|
This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days. |
An experimental feature isn't a final product. It's getting it into the hands of users to get real usage feedback. Because it's experimental, it means changes can happen without them being breaking changes. Can you summarize what questions you want answered? |
| { "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) }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trace-Script seems like a better name for what it does currently. A separate Measure-Script may be needed later that leverages Trace-Script, but produces summarized output to the user. I don't think Measure-Script needs to be part of this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree.
Measure-Script is the name of the main cmdlet in PSProfiler. Trace-Script is the name of the main cmdlet in my Profiler module, but only because Measure-Script is already taken. Both these cmdlets provide a summary.
So using Trace-Script here to get the "raw" data, is imho the best choice. And I am okay with renaming mine.
|
This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't review code in Profiler.cs yet, but want to share my feedback so far.
Can you summarize what questions you want answered?
I want to echo on this. @iSazonov can you please summarize those questions again?
I think we need to scope the functionalities we want in this PR. Based on @nohwnd's comments above, it looks the Pester coverage functionality is mostly working with the changes. So, can we set that to be the goal of this PR? (That means we only accept the features/changes needed for Pester coverage and nothing else.)
| } | ||
|
|
||
| internal static ScriptBlock TryGetCachedScriptBlock(string fileName, string fileContents) | ||
| internal static CompiledScriptBlockData TryGetCachedCompiledScriptBlock(string fileName, string fileContents) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about keeping the method signature unchanged, but only changing the cache to hold CompiledScriptBlockData instead? In that way, the call sites of the TryGetCacheXXX and CacheXXX methods are unchanged. It's really just an implementation detail of those 2 methods.
| CompiledScriptBlockData compiledScriptBlockData = ScriptBlock.TryGetCachedCompiledScriptBlock(_path, scriptContents); | ||
| if (compiledScriptBlockData != null) | ||
| { | ||
| this.ScriptBlock = new ScriptBlock(compiledScriptBlockData); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should avoid making changes to the call sites of TryGetCachedScriptBlock. Whether it caches ScriptBlock or CompiledScriptBlockData should be an implementation detail to TryGetCachedScriptBlock.
| { | ||
| this.ScriptBlock = new ScriptBlock(_scriptBlockAst, isFilter: false); | ||
| ScriptBlock.CacheScriptBlock(_scriptBlock.Clone(), _path, scriptContents); | ||
| ScriptBlock.CacheCompiledScriptBlock(_scriptBlock.Clone(), _path, scriptContents); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
| // 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please reword the comments here? The algorithm is not clear to me by reading the existing comments. An example would be helpful too.
| internal void UpdatePositionNoBreak(int pos) | ||
| { | ||
| _currentSequencePointIndex = pos; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, we only emit events when _checkBreakpoints == true? Why not when it's false, and in what cases _checkBreakpoints would be false?
Also, then this method doesn't really do anything and should be removed.
| MethodInfo method = _checkBreakpoints | ||
| ? CachedReflectionInfo.FunctionContext_UpdatePosition | ||
| : CachedReflectionInfo.FunctionContext_UpdatePositionNoBreak; | ||
| exprs.Add(Expression.Call(Compiler.s_functionContext, method, ExpressionCache.Constant(_sequencePoint))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be changed to:
exprs.Add(
Expression.Assign(
Expression.Field(Compiler.s_functionContext, CachedReflectionInfo.FunctionContext__currentSequencePointIndex),
ExpressionCache.Constant(_sequencePoint)));
if (_checkBreakpoints)
{
exprs.Add(Expression.Call(Compiler.s_functionContext, CachedReflectionInfo.FunctionContext_UpdatePosition);
}
exprs.Add(ExpressionCache.Empty);| internal class CompiledScriptBlockData | ||
| { | ||
| private static readonly ConcurrentDictionary<CompiledScriptBlockData, CompiledScriptBlockData> s_compiledScriptBlockTable | ||
| = new ConcurrentDictionary<CompiledScriptBlockData, CompiledScriptBlockData>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do the key and value are the same object? Comments are needed here to explain the intent.
| => 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, better make the signature unchanged. What to actually cache is an implementation detail.
| [EventSource(Name = "Microsoft-PowerShell-Profiler")] | ||
| internal class ProfilerEventSource : EventSource | ||
| { | ||
| internal static ProfilerEventSource LogInstance = new ProfilerEventSource(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other event source classes use the name Log for this field: https://github.com/PowerShell/PowerShell/blob/master/src/System.Management.Automation/engine/parser/Parser.cs#L8260
So, better to keep consistent.
| // 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this for getting the script call stack when generating events? It looks quite hacky.
I think we need to define the goal for this PR. I guess the goal is to make the Pester's coverage functionality able to work with the ETW events generated from the changes here. So, is the script stack necessary for the Pester's coverage functionality to work?
If we set that as the goal, we should only include features/changes that are necessary for that to work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both Pester code coverage, and Profiler would depend on this. Pester code coverage is a subset of Profiler functionality. Both use this interface to provide their services:
https://github.com/nohwnd/Profiler/blob/main/csharp/Profiler/ITracer.cs
Which collects:
// The currently executing scriptblock and extent
IScriptExtent extent,
ScriptBlock scriptBlock,
// how many items are on callstack
int level,
// the current function and module name
string functionName,
string moduleName
The data is collected here, by reflecting on debugger, functionContext and session state:
https://github.com/nohwnd/Profiler/blob/main/csharp/Profiler/Tracer.cs#L157-L171
As long as this info is provided in some form, the current version of Profiler can work with this.
"Level" I should be able to get from scriptblock ids as pointed out here: #13673 (comment)
Module I should be able to get from the scriptblock and its associated session state. I think.
But what about the function name? Any suggestions where I can grab that?
|
Commented above, if the current implementation adds function name (or someone tells me where to get its😇 ), I should be good to integrate this with Profiler. Since we last talked a year ago my Profiler module evolved a bit and now collects also module and function name (from session state and current function context). Which makes me think that exposing this as a list of events with predefined data might be limiting in the future. Could there be also an additional extension point similar to There are few general objects that might potentially have more interesting info, and it would be cool to have a built-in extension point that I could use in the future, to look at the current script extent, scriptblock, session state, function context, and callstack. |
|
@daxian-dbw This project has always been a hobby of mine, but waiting more than a year and a half is no longer a hobby, but a frustrating disappointment. I hope you understand that I don't have any motivation to remember this old story, especially since I found several other projects that I'm happy to spend my time on. I have already closed most of my unanswered PRs. Since no one was upset, it means no one needs them. The ones I left may be useful to someone or you can close them. I always saw in PowerShell a huge potential to change the ecosystem. Unfortunately this is in a deep sleep and I don't expect it to change in the foreseeable future unfortunately. |
|
@iSazonov I totally understand your frustration and I'm truly sorry seeing this happening. |
|
@daxian-dbw The sole purpose of this PR was to implement the idea of Json Shirk so that we could explore it. |
|
My current solution works okay, except that it can't profile additional runspaces (not sure if that is a limitation I will be able to lift easily). Unless @JustinGrote wants to push this forward, I am okay with this PR being closed. |
Events should be generated in Engine. Rest of processing can be external. |
|
@iSazonov I appreciate all the work you've done and we've been trying to have Mondays focused on community contributions since the start of this month so that at least we have a more regular cadence when folks can expect an ack or response. For this PR, I think the maintainers should take it over to get the events into 7.3 and the processing of the events will be an external module. |
I agree, add the API to be able to get the profiling events and leave the implementation up to the community. |
|
This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days. |
|
The Engine Working Group discussed this PR in the last meeting (7/7), and we agreed to close it with the following reasons:
Thank you @iSazonov for putting up this PR and led the discussion around the idea of emitting events during script execution. We can always reference this PR when a similar need comes up in future. |
|
@daxian-dbw @SeeminglyScience In the PR was made some helper changes in Engine. Could you please review them? Perhaps this is of some value for other scenarios. If not, I will delete this branch altogether. |
PR Summary
Simple demo:
PR Context
Based on Jason's approach lzybkr@d620bb4
Perf event (with minimal data) issues for every sequence point while measure runs
Perf events are implemented in ProfilerEventSource class
Perf event contains
Now perf events are issued for default parameter evaluations (and more).
Limitations:
Rundown event (with full meta data) issues for every sequence point after measure stops
Rundown event issues for every script block (not used in Measure-Script now)
Rundown event issues are implemented in ProfilerRundownEventSource class
Scriptblock cache was simplifed and now it caches
CompiledScriptBlock-snew ScriptBlock(CompiledScriptBlock)new ScriptBlock(CompiledScriptBlock)without extra cloning.To collect meta data we use
ConcurrentDictionaryCompiledScriptBlock.s_cachedScriptscacheTo initialize the cache at measure start we use CompiledScriptBlock cache and then register every script block at creation time.
This allow us to have meta data for every script block.
Exception (an edge case) - long running script blocks already removed from CompiledScriptBlock cache.
Measure-Script cmdlet is implemented for demo use
ProfileEventRecord is used for typed output
Duration is broken in common
For discussion
Since we have only start timestamps we haven't stop timestamp for last sequence point. Perhaps we need a script block stop event and maybe start event.
Here a question is should we think about measuring pipelines, parameter bindings.
Also can we be compatible with PerfView tool?
PR Checklist
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright headerWIP:or[ WIP ]to the beginning of the title (theWIPbot will keep its status check atPendingwhile the prefix is present) and remove the prefix when the PR is ready.