Skip to content
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

Add new ParseError level to ScriptFileMarkerLevel and only have it send parse errors #888

Merged

Conversation

TylerLeonhardt
Copy link
Member

@TylerLeonhardt TylerLeonhardt commented Mar 22, 2019

In PSScriptAnalyzer 1.18, they've added parse errors to the diagnostic records that get sent back from PSSA.

This broke us because they come in as a different ScriptFileMarkerLevel called ParseError that we didn't handle.

This PR adds that additional enum member and filters PSSA invocations to only get back all diagnostics other than ParseErrors. PSES still handles its own ParseErrors so that (1) there's no startup cost to ParseErrors, (2) ParseErrors get shown no matter whether PSSA is available.

cc @bergmeister

@TylerLeonhardt TylerLeonhardt changed the title Add new parseerror sev Add new ParseError level to ScriptFileMarkerLevel and only have it send parse errors Mar 22, 2019
@rkeithhill
Copy link
Contributor

I'll try to fully review this tomorrow but one concern I have is that if someone turns off codeAnalysis, they'll lose parse errors now, right? Or does this fallback to using the PSES parse error diagnostics if code analysis is disabled. Oh yeah, maybe two concerns - PSSA performance - I get really tired of waiting for PSSA diagnostic markers catching up with my code edits in large files. Squiggles often display on the wrong lines for minutes some times. I'd hate to not get updated on parse errors for minutes. That could mean I've committed a file with a parse error. :-(

@rkeithhill
Copy link
Contributor

rkeithhill commented Mar 22, 2019

Now that I think about, I'd almost prefer that we disable that rule (or ignore the marker) and rely on our parse error diagnostics because performance. I timed v1.18 on the InvokePlaster.ps1 file and it still takes 15 secs to complete. Seems like the response time on parse errors should be much faster than that.

{
throw new ArgumentException(
string.Format(
"The provided DiagnosticSeverity value '{0}' is unknown.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit: Using string interpolation ($"") we could merge 3 lines into 1 whilst having better readability.

@bergmeister
Copy link
Contributor

bergmeister commented Mar 22, 2019

@rkeithhill The new version should be twice as fast for cold starts and magnitudes faster when re-analyzing. I am not sure if this is a PSES or PSSA issue that refreshes sometimes take a long time or require more typing.
I will take a look at your file and see if it is somehow special.
On my machine (4 core laptop i7), I can analyse the build.psm1 of the PowerShell/Power repo in less than 10 seconds and this file has thousands of lines of code now.
I also have a PR open for 1.19 to halve execution time further for cold starts btw.

@JamesWTruher
Copy link
Member

JamesWTruher commented Mar 22, 2019

@rkeithhill was that 15-second delay a cold start, or every time you pressed a key? If it was just cold-start, how would you characterize performance after cold-start?

@JamesWTruher
Copy link
Member

I did a quick check on my mac with 1.18, cold-start definitely takes longer than I like, here is what I saw:
(cold-start, followed by warm-start)

PS> measure-command { invoke-scriptanalyzer ./InvokePlaster.ps1 }
Seconds           : 8
Milliseconds      : 889
Ticks             : 88895066
TotalSeconds      : 8.8895066
TotalMilliseconds : 8889.5066

PS> measure-command { invoke-scriptanalyzer ./InvokePlaster.ps1 }
Seconds           : 0
Milliseconds      : 68
Ticks             : 681868
TotalSeconds      : 0.0681868
TotalMilliseconds : 68.1868

@bergmeister
Copy link
Contributor

bergmeister commented Mar 22, 2019

I did a comparison of PSSA 1.17.1 and 1.18.0 and the time (cold-start) went down from 28.7 to 14.3 seconds, thereby proving what we promised with the new release. Furthermore the time of a re-run of the same file went down from 11 to 0.1 seconds.
The following PR can nearly halve the time for cold-runs further to 8.9 seconds: PowerShell/PSScriptAnalyzer#1178
The bottleneck for the analysis of cold runs are the calls to Get-Command that 5 rules make and also how slow the slowest rule is (the above PR speeds up the slowest rule). I once did an experiment to just initialise the CommandInfo cache once in a background thread by calling Get-Command once but due this PowerShell issue, I could not use the results: PowerShell/PowerShell#8910

@rkeithhill
Copy link
Contributor

rkeithhill commented Mar 22, 2019

Fair point, my second invocation does drop to < 1 second.

How does your mac behave (what's the CPU usage) while this runs:

for ($i = 0; $i -lt 100; $i++) { Measure-Command { Invoke-ScriptAnalyzer -Settings ..\ScriptAnalyzerSettings.psd1 -Path .\InvokePlaster.ps1 }; Start-Sleep -Milliseconds 250 }

This simulates someone typing in a commandline of 100 chars where keystrokes are spaced about 250 milliseconds apart (OK, they are a fast-ish typist). What I see on my laptop (apparently twice as slow as your mac) is that while this runs, my CPU sits at 10%. That's not horrible but not great either.

That said, we do have code in the extension that anticipates this scenario (delayThenInvokeDiagnostics) that should help here. Anyway, rather that characterize perf in isolation, I think we need to eval the perf of this running in the extension on some good size files and not only loading but editing them to see what the experience is like. Keeping track of how long a parse error diag takes to show up and what the CPU usage is. We've already gotten complaints about CPU usage.

@rkeithhill
Copy link
Contributor

rkeithhill commented Mar 22, 2019

BTW the perf work & results on PSSA are great! Great job there. It's just that use with the extension is perhaps a bit abusive (runs a lot). Also we have other issues with the the message processing getting behind due to its sequential nature and the fact that we dont' handle cancelRequests. I have a PR open to attempt to address the cancelRequests but it hasn't gotten any traction yet.

@TylerLeonhardt
Copy link
Member Author

@JamesWTruher and I talked about this and agree with @rkeithhill in having PSES handle the parse errors... but we agree for a different reason.

One of the things @rjmholt, @JamesWTruher and I have been set out to do was switch to using PSSA via a C# API that @JamesWTruher has been working on (not quite done yet).

One of the requirements for that work is the ability to hand PSSA an AST so that the rules can be run on that because PSES already has the AST.

If you pass something the AST, it can't get the parse errors so PSES will have to send parse errors from the initial parsing of the file anyway.

I'll filter out the ParseErrors for now and continue sending them via PSES in preparation for PSSA as an API that accepts an AST.

@rkeithhill
Copy link
Contributor

Ooh, I like the idea of using the API directly. That cuts out a bunch of PowerShell invocation goo and redundant parsing! Plus doing it this way means we still get parse error diags even if someone chooses to disable code analysis.

@bergmeister
Copy link
Contributor

Yes, the high level plan is to publish a NuGet package of PSSA (Engine and Rules) that multi-targets net452 and netstandard2.0. Due to conditional compilation we will have to drop support for PS version 3 and 4 though. Then PSES could reference this package (and also reduce code duplication) and call into the API's directly instead of calling the cmdlets via PowerShell. In the past folks have reported memory issue though but I hope this should not happen any more. I ran PSSA against the whole test folder of the PowerShell repo, which results in 800MB of memory usage but it does not grow when re-analysing (I am calling [GC}::Collect(). I suspect the size is mainly due to CommandInfo being a bit wasteful with memory similar to PSObject.
With the NuGet package, people can still install different version of PSSA and there is less coupling (we could release updates to the NuGet package to the extension first to test out coming changes before publishing PSSA

@TylerLeonhardt
Copy link
Member Author

I've updated the PR to use parse errors from PSES.

@@ -162,36 +164,27 @@ internal static ScriptFileMarker FromDiagnosticRecord(PSObject psObject)
};
}

string severity = diagnosticRecord.Severity.ToString();
if(!Enum.TryParse(severity, out ScriptFileMarkerLevel level))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit, space between if and opening (.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm glad I wasn't the only one that felt like this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - fixed.

}

scriptFile.SyntaxMarkers.AddRange(semanticMarkers);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want to modify the SyntaxMarkers collection in ScriptFile by adding semantic markers? Maybe we should have two collections in ScriptFile (SyntaxMarkers and SemanticMarkers) or perhaps rename the property to DiagnosticMarkers (or something like that).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with the rename since the syntax markers and semantic markers are both treated the same when they get sent to vscode.

Copy link
Contributor

@rkeithhill rkeithhill left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@@ -55,7 +55,7 @@ public class AnalysisService : IDisposable
/// <summary>
/// An empty script marker result to return when no script markers can be returned.
/// </summary>
private static readonly ScriptFileMarker[] s_emptyScriptMarkerResult = new ScriptFileMarker[0];
private static readonly List<ScriptFileMarker> s_emptyScriptMarkerResult = new List<ScriptFileMarker>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is supposed to be a readonly empty value, it can't be a List<T>.

Otherwise, if we need to return a mutable collection, we should either:

  • Return a readonly collection here and create a new mutable collection around it in the caller if it needs extending, or;
  • Return a List and not have an s_emptyScriptMarkerResult, but instead return a new List<T>() every time.

If we return s_emptyScriptMarkerResult and the caller then mutates it, those additions will become permanently added to s_emptyScriptMarkerResult.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point. I've switched to just some new lists.

@TylerLeonhardt TylerLeonhardt merged commit 2f79bcf into PowerShell:master Mar 25, 2019
@TylerLeonhardt TylerLeonhardt deleted the add-new-parseerror-sev branch March 25, 2019 18:57
TylerLeonhardt added a commit to TylerLeonhardt/PowerShellEditorServices that referenced this pull request Mar 25, 2019
…erLevel and only have it send parse errors
TylerLeonhardt added a commit that referenced this pull request Mar 26, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants