Skip to content

Commit

Permalink
[Profiling] Add infra to run port forwarding tunnels
Browse files Browse the repository at this point in the history
Dive, RGP and Renderdoc expect SSH port forwarding processes to be
present when the game starts. Right now, these processes are started
from YetiDebugTransport. This code is only run when a debugger is
attached. However, it is not a good idea to profile with debugger
attached, as that deteriorates performance. Hence, the port forwarding
processes should be started whenever the game is started, whether a
debugger is attached or not.

This CL is prep work for that. It adds
- SshTunnelProcess: Wraps an SSH port forwarding process.
- GameLifetimeWatcher: Triggers a callback when the game shuts down.
When the game shuts down, the tunnels are shut down as well.

SshTunnelManager is the overall manager class that coordinates
spinning up and shutting down tunnels.

Also moves the ManagedProcess.OnExit call to after logging, as the
handlers might dispose the process, so ExitCode isn't valid anymore.

This CL does not affect current behavior, the new code is not yet
hooked up.

GitOrigin-RevId: ca4627d6473daf5a5906f4b77b37d8a89219066d
  • Loading branch information
ljusten authored and copybara-github committed Mar 18, 2022
1 parent 8e5de8d commit 74520cb
Show file tree
Hide file tree
Showing 11 changed files with 1,037 additions and 6 deletions.
2 changes: 0 additions & 2 deletions YetiCommon/ChromeClientsLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,6 @@ public Factory(ChromeClientLaunchCommandFormatter launchCommandFormatter,
_chromeLauncher);
}

const string _debugModeValue = "2";

readonly Lazy<SdkConfig> _sdkConfig;
SdkConfig SdkConfig => _sdkConfig.Value;

Expand Down
5 changes: 3 additions & 2 deletions YetiCommon/ManagedProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ public interface IProcess : IDisposable
/// <remarks>
/// Output and error data handlers are guaranteed to be called before this task completes.
/// </remarks>
/// <returns>The process exit code.</returns>
Task<int> RunToExitAsync();

/// <summary>
Expand Down Expand Up @@ -442,8 +443,6 @@ void ExitHandler(object sender, EventArgs args)
return;
}

OnExit?.Invoke(this, args);

try
{
Trace.WriteLine($"Process {ProcessName} [{Id}] exited with code {ExitCode}");
Expand All @@ -454,6 +453,8 @@ void ExitHandler(object sender, EventArgs args)
Trace.WriteLine($"Failed to read an exit code of the process {ProcessName} " +
$"[{Id}] due to `{exception.Message}`, the process is already disposed.");
}

OnExit?.Invoke(this, args);
}

public StreamReader StandardOutput => _process.StandardOutput;
Expand Down
133 changes: 133 additions & 0 deletions YetiVSI.Tests/Profiling/GameLifetimeWatcherTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Threading;
using System.Threading.Tasks;
using GgpGrpc.Models;
using Microsoft.VisualStudio.Threading;
using NSubstitute;
using NUnit.Framework;
using YetiVSI.GameLaunch;
using YetiVSI.Profiling;

namespace YetiVSI.Test.Profiling
{
class GameLifetimeWatcherTests
{
static readonly TimeSpan _startupTimeout = TimeSpan.FromSeconds(60);

static readonly GgpGrpc.Models.GameLaunch _launchPrepping = new GgpGrpc.Models.GameLaunch
{ GameLaunchState = GameLaunchState.ReadyToPlay, GameletName = "gamelet" };

static readonly GgpGrpc.Models.GameLaunch _launchRunning = new GgpGrpc.Models.GameLaunch
{ GameLaunchState = GameLaunchState.RunningGame, GameletName = "gamelet" };

static readonly GgpGrpc.Models.GameLaunch _launchEnded = new GgpGrpc.Models.GameLaunch
{
GameLaunchState = GameLaunchState.GameLaunchEnded,
GameLaunchEnded = new GameLaunchEnded(EndReason.ExitedByUser),
GameletName = "gamelet"
};

IVsiGameLaunch _launch;
AsyncManualResetEvent _onDoneCalled;
GameLifetimeWatcher.DoneHandler _onDone;
GameLifetimeWatcher _watcher;

[SetUp]
public void SetUp()
{
_launch = Substitute.For<IVsiGameLaunch>();
_onDoneCalled = new AsyncManualResetEvent();
_watcher = new GameLifetimeWatcher();
_onDone = Substitute.For<GameLifetimeWatcher.DoneHandler>();
_onDone.When(x => x.Invoke(_watcher, Arg.Any<bool>(), Arg.Any<string>()))
.Do(x => _onDoneCalled.Set());
}

[Test]
public void StopWithoutStart()
{
_watcher.Stop();
}

[Test]
public void StartStartThrows()
{
_launch.GetLaunchStateAsync(null).Returns(Task.FromResult(_launchPrepping));
_watcher.Start(_launch, _startupTimeout, _onDone);
Assert.Throws(typeof(InvalidOperationException),
() => _watcher.Start(_launch, _startupTimeout, _onDone));
}

[Test]
public void StartStopNotLaunched()
{
_launch.GetLaunchStateAsync(null).Returns(Task.FromResult(_launchPrepping));

_watcher.Start(_launch, _startupTimeout, _onDone);
_watcher.Stop();

_onDone.DidNotReceive().Invoke(_watcher, Arg.Any<bool>(), Arg.Any<string>());
}

[Test]
public void DoubleStop()
{
_launch.GetLaunchStateAsync(null).Returns(Task.FromResult(_launchPrepping));
_watcher.Start(_launch, _startupTimeout, _onDone);

_watcher.Stop();
_watcher.Stop();
}

[Test]
public async Task StartLaunchedSucceedsAsync()
{
_launch.GetLaunchStateAsync(null).Returns(Task.FromResult(_launchEnded));

_watcher.Start(_launch, _startupTimeout, _onDone);

await _onDoneCalled.WaitAsync().WithTimeout(TimeSpan.FromSeconds(5));
_onDone.Received().Invoke(_watcher, true, null);
}

[Test]
public async Task StartupTimesOutWhenNotRunningAsync()
{
_launch.GetLaunchStateAsync(null).Returns(Task.FromResult(_launchPrepping));

_watcher.Start(_launch, TimeSpan.Zero, _onDone);

await _onDoneCalled.WaitAsync().WithTimeout(TimeSpan.FromSeconds(5));
_onDone.Received().Invoke(_watcher, false,
Arg.Do<string>(errorMsg =>
StringAssert.Contains(
"Timed out", errorMsg)));
}

[Test]
public void StartupDoesNotTimeOutWhenRunning()
{
_launch.GetLaunchStateAsync(null).Returns(Task.FromResult(_launchRunning));

_watcher.Start(_launch, TimeSpan.Zero, _onDone);

// How do you test that something will never happen?
Thread.Sleep(10);
Assert.False(_onDoneCalled.IsSet);
}
}
}
Loading

0 comments on commit 74520cb

Please sign in to comment.