Skip to content

Throttle semantic analysis requests for better performance #20

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

Merged
merged 3 commits into from
Sep 16, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@
using Microsoft.PowerShell.EditorServices.Session;
using Microsoft.PowerShell.EditorServices.Transport.Stdio.Event;
using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message;
using Microsoft.PowerShell.EditorServices.Utility;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Request
{
[MessageTypeName("geterr")]
public class ErrorRequest : RequestBase<ErrorRequestArguments>
{
private static CancellationTokenSource existingRequestCancellation;

public static ErrorRequest Create(params string[] filePaths)
{
return new ErrorRequest
Expand All @@ -30,8 +36,78 @@ public override void ProcessMessage(
{
List<ScriptFile> fileList = new List<ScriptFile>();

// If there's an existing task, attempt to cancel it
try
{
if (existingRequestCancellation != null)
{
// Try to cancel the request
existingRequestCancellation.Cancel();

// If cancellation didn't throw an exception,
// clean up the existing token
existingRequestCancellation.Dispose();
existingRequestCancellation = null;
}
}
catch (Exception e)
{
// TODO: Catch a more specific exception!
Logger.Write(
LogLevel.Error,
string.Format(
"Exception while cancelling analysis task:\n\n{0}",
e.ToString()));

return;
}

// Create a fresh cancellation token and then start the task.
// We create this on a different TaskScheduler so that we
// don't block the main message loop thread.
// TODO: Is there a better way to do this?
existingRequestCancellation = new CancellationTokenSource();
Task.Factory.StartNew(
() =>
DelayThenInvokeDiagnostics(
this.Arguments.Delay,
this.Arguments.Files,
editorSession,
messageWriter,
existingRequestCancellation.Token),
CancellationToken.None,
TaskCreationOptions.None,
TaskScheduler.Default);
}

private static async Task DelayThenInvokeDiagnostics(
int delayMilliseconds,
string[] filesToAnalyze,
EditorSession editorSession,
MessageWriter messageWriter,
CancellationToken cancellationToken)
{
// First of all, wait for the desired delay period before
// analyzing the provided list of files
try
{
await Task.Delay(delayMilliseconds, cancellationToken);
}
catch (TaskCanceledException)
{
// If the task is cancelled, exit directly
return;
}

// If we've made it past the delay period then we don't care
// about the cancellation token anymore. This could happen
// when the user stops typing for long enough that the delay
// period ends but then starts typing while analysis is going
// on. It makes sense to send back the results from the first
// delay period while the second one is ticking away.

// Get the requested files
foreach (string filePath in this.Arguments.Files)
foreach (string filePath in filesToAnalyze)
{
ScriptFile scriptFile =
editorSession.Workspace.GetFile(
Expand Down
2 changes: 1 addition & 1 deletion src/PowerShellEditorServices.Transport.Stdio/StdioHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ async Task ListenForMessages()
MessageReader messageReader =
new MessageReader(
System.Console.In,
MessageFormat.WithoutContentLength,
MessageFormat.WithContentLength,
messageTypeResolver);

MessageWriter messageWriter =
Expand Down
56 changes: 46 additions & 10 deletions src/PowerShellEditorServices/Utility/Logger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,16 @@ public LogWriter(LogLevel minimumLogLevel, string logFilePath, bool deleteExisti
logFilePath);
}

// Open the log file for writing with UTF8 encoding
this.textWriter =
new StreamWriter(
new FileStream(
logFilePath,
deleteExisting ?
FileMode.Create :
FileMode.Append),
Encoding.UTF8);
if (!this.TryOpenLogFile(logFilePath, deleteExisting))
{
// If the log file couldn't be opened at this location,
// try opening it in a more reliable path
this.TryOpenLogFile(
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
Path.GetFileName(logFilePath)),
deleteExisting);
}
}

public void Write(
Expand All @@ -144,7 +145,8 @@ public void Write(
string callerSourceFile = null,
int callerLineNumber = 0)
{
if (logLevel >= this.minimumLogLevel)
if (this.textWriter != null &&
logLevel >= this.minimumLogLevel)
{
// Print the timestamp and log level
this.textWriter.WriteLine(
Expand Down Expand Up @@ -176,5 +178,39 @@ public void Dispose()
this.textWriter = null;
}
}

private bool TryOpenLogFile(
string logFilePath,
bool deleteExisting)
{
try
{
// Open the log file for writing with UTF8 encoding
this.textWriter =
new StreamWriter(
new FileStream(
logFilePath,
deleteExisting ?
FileMode.Create :
FileMode.Append),
Encoding.UTF8);

return true;
}
catch (Exception e)
{
if (e is UnauthorizedAccessException ||
e is IOException)
{
// This exception is thrown when we can't open the file
// at the path in logFilePath. Return false to indicate
// that the log file couldn't be created.
return false;
}

// Unexpected exception, rethrow it
throw;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public void Start()
this.MessageWriter =
new MessageWriter(
this.languageServiceProcess.StandardInput,
MessageFormat.WithoutContentLength,
MessageFormat.WithContentLength,
messageTypeResolver);

// Wait for the 'started' event
Expand Down