Skip to content

Commit 9aee741

Browse files
committed
Add intervallic buffering of OutputEvent messages
This change introduces a new AsyncDebouncer class which is able to gate invocation requests to only occur after a specified interval. This class is used to implement an OutputDebouncer which takes all output messages written from the PowerShell host and buffers them. This causes many lines of output written in a short time window to be sent as one OutputEvent rather than writing individual events for each line. Resolves #113.
1 parent 5c69c7d commit 9aee741

File tree

15 files changed

+672
-54
lines changed

15 files changed

+672
-54
lines changed

src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
<Compile Include="Properties\AssemblyInfo.cs" />
125125
<Compile Include="LanguageServer\References.cs" />
126126
<Compile Include="Server\LanguageServerSettings.cs" />
127+
<Compile Include="Server\OutputDebouncer.cs" />
127128
<Compile Include="Server\PromptHandlers.cs" />
128129
</ItemGroup>
129130
<ItemGroup>

src/PowerShellEditorServices.Protocol/Properties/AssemblyInfo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@
4141
[assembly: AssemblyFileVersion("0.0.0.0")]
4242
[assembly: AssemblyInformationalVersion("0.0.0.0")]
4343

44+
[assembly: InternalsVisibleTo("Microsoft.PowerShell.EditorServices.Test.Protocol")]

src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter;
77
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol;
88
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel;
9-
using Microsoft.PowerShell.EditorServices.Protocol.Server;
109
using Microsoft.PowerShell.EditorServices.Utility;
1110
using System;
1211
using System.Collections.Generic;
@@ -19,6 +18,7 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.Server
1918
public class DebugAdapter : DebugAdapterBase
2019
{
2120
private EditorSession editorSession;
21+
private OutputDebouncer outputDebouncer;
2222

2323
public DebugAdapter() : this(new StdioServerChannel())
2424
{
@@ -30,6 +30,9 @@ public DebugAdapter(ChannelBase serverChannel) : base(serverChannel)
3030
this.editorSession.StartSession();
3131
this.editorSession.DebugService.DebuggerStopped += this.DebugService_DebuggerStopped;
3232
this.editorSession.ConsoleService.OutputWritten += this.powerShellContext_OutputWritten;
33+
34+
// Set up the output debouncer to throttle output event writes
35+
this.outputDebouncer = new OutputDebouncer(this);
3336
}
3437

3538
protected override void Initialize()
@@ -59,6 +62,9 @@ protected override void Initialize()
5962

6063
protected override void Shutdown()
6164
{
65+
// Make sure remaining output is flushed before exiting
66+
this.outputDebouncer.Flush().Wait();
67+
6268
Logger.Write(LogLevel.Normal, "Debug adapter is shutting down...");
6369

6470
if (this.editorSession != null)
@@ -359,6 +365,9 @@ await requestContext.SendResult(
359365

360366
async void DebugService_DebuggerStopped(object sender, DebuggerStopEventArgs e)
361367
{
368+
// Flush pending output before sending the event
369+
await this.outputDebouncer.Flush();
370+
362371
await this.SendEvent(
363372
StoppedEvent.Type,
364373
new StoppedEventBody
@@ -376,13 +385,8 @@ await this.SendEvent(
376385

377386
async void powerShellContext_OutputWritten(object sender, OutputWrittenEventArgs e)
378387
{
379-
await this.SendEvent(
380-
OutputEvent.Type,
381-
new OutputEventBody
382-
{
383-
Output = e.OutputText + (e.IncludeNewLine ? "\r\n" : string.Empty),
384-
Category = (e.OutputType == OutputType.Error) ? "stderr" : "stdout"
385-
});
388+
// Queue the output for writing
389+
await this.outputDebouncer.Invoke(e);
386390
}
387391

388392
#endregion

src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class LanguageServer : LanguageServerBase
2525
private static CancellationTokenSource existingRequestCancellation;
2626

2727
private EditorSession editorSession;
28+
private OutputDebouncer outputDebouncer;
2829
private LanguageServerSettings currentSettings = new LanguageServerSettings();
2930

3031
public LanguageServer() : this(new StdioServerChannel())
@@ -44,6 +45,9 @@ public LanguageServer(ChannelBase serverChannel) : base(serverChannel)
4445
new ProtocolPromptHandlerContext(
4546
this,
4647
this.editorSession.ConsoleService));
48+
49+
// Set up the output debouncer to throttle output event writes
50+
this.outputDebouncer = new OutputDebouncer(this);
4751
}
4852

4953
protected override void Initialize()
@@ -78,6 +82,9 @@ protected override void Initialize()
7882

7983
protected override void Shutdown()
8084
{
85+
// Make sure remaining output is flushed before exiting
86+
this.outputDebouncer.Flush().Wait();
87+
8188
Logger.Write(LogLevel.Normal, "Language service is shutting down...");
8289

8390
if (this.editorSession != null)
@@ -757,13 +764,8 @@ protected Task HandleEvaluateRequest(
757764

758765
async void powerShellContext_OutputWritten(object sender, OutputWrittenEventArgs e)
759766
{
760-
await this.SendEvent(
761-
DebugAdapterMessages.OutputEvent.Type,
762-
new DebugAdapterMessages.OutputEventBody
763-
{
764-
Output = e.OutputText + (e.IncludeNewLine ? "\r\n" : string.Empty),
765-
Category = (e.OutputType == OutputType.Error) ? "stderr" : "stdout"
766-
});
767+
// Queue the output for writing
768+
await this.outputDebouncer.Invoke(e);
767769
}
768770

769771
#endregion
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter;
7+
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol;
8+
using Microsoft.PowerShell.EditorServices.Utility;
9+
using System.Threading.Tasks;
10+
11+
namespace Microsoft.PowerShell.EditorServices.Protocol.Server
12+
{
13+
/// <summary>
14+
/// Throttles output written via OutputEvents by batching all output
15+
/// written within a short time window and writing it all out at once.
16+
/// </summary>
17+
internal class OutputDebouncer : AsyncDebouncer<OutputWrittenEventArgs>
18+
{
19+
#region Private Fields
20+
21+
private IMessageSender messageSender;
22+
private bool currentOutputIsError = false;
23+
private string currentOutputString = null;
24+
25+
#endregion
26+
27+
#region Constants
28+
29+
// Set a really short window for output flushes. This
30+
// gives the appearance of fast output without the crushing
31+
// overhead of sending an OutputEvent for every single line
32+
// written. At this point it seems that around 10-20 lines get
33+
// batched for each flush when Get-Process is called.
34+
public const int OutputFlushInterval = 200;
35+
36+
#endregion
37+
38+
#region Constructors
39+
40+
public OutputDebouncer(IMessageSender messageSender)
41+
: base(OutputFlushInterval, false)
42+
{
43+
this.messageSender = messageSender;
44+
}
45+
46+
#endregion
47+
48+
#region Private Methods
49+
50+
protected override async Task OnInvoke(OutputWrittenEventArgs output)
51+
{
52+
bool outputIsError = output.OutputType == OutputType.Error;
53+
54+
if (this.currentOutputIsError != outputIsError)
55+
{
56+
if (this.currentOutputString != null)
57+
{
58+
// Flush the output
59+
await this.OnFlush();
60+
}
61+
62+
this.currentOutputString = string.Empty;
63+
this.currentOutputIsError = outputIsError;
64+
}
65+
66+
// Output string could be null if the last output was already flushed
67+
if (this.currentOutputString == null)
68+
{
69+
this.currentOutputString = string.Empty;
70+
}
71+
72+
// Add to string (and include newline)
73+
this.currentOutputString +=
74+
output.OutputText +
75+
(output.IncludeNewLine ?
76+
System.Environment.NewLine :
77+
string.Empty);
78+
}
79+
80+
protected override async Task OnFlush()
81+
{
82+
// Only flush output if there is some to flush
83+
if (this.currentOutputString != null)
84+
{
85+
// Send an event for the current output
86+
await this.messageSender.SendEvent(
87+
OutputEvent.Type,
88+
new OutputEventBody
89+
{
90+
Output = this.currentOutputString,
91+
Category = (this.currentOutputIsError) ? "stderr" : "stdout"
92+
});
93+
94+
// Clear the output string for the next batch
95+
this.currentOutputString = null;
96+
}
97+
}
98+
99+
#endregion
100+
}
101+
}
102+

src/PowerShellEditorServices/PowerShellEditorServices.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
<Compile Include="Session\SessionPSHostUserInterface.cs" />
102102
<Compile Include="Session\SessionStateChangedEventArgs.cs" />
103103
<Compile Include="Utility\AsyncContextThread.cs" />
104+
<Compile Include="Utility\AsyncDebouncer.cs" />
104105
<Compile Include="Utility\AsyncLock.cs" />
105106
<Compile Include="Utility\AsyncQueue.cs" />
106107
<Compile Include="Utility\AsyncContext.cs" />

0 commit comments

Comments
 (0)