Skip to content

Commit 4dd8e7c

Browse files
committed
Delay non-essential startup tasks to improve performance
1 parent e94fb49 commit 4dd8e7c

File tree

5 files changed

+103
-24
lines changed

5 files changed

+103
-24
lines changed

src/WebJobs.Script.WebHost/FileMonitoringService.cs

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ namespace Microsoft.Azure.WebJobs.Script.WebHost
2323
{
2424
public class FileMonitoringService : IHostedService, IDisposable
2525
{
26+
private const int _delayedWatchersDelaySeconds = 5;
2627
private readonly ScriptJobHostOptions _scriptOptions;
2728
private readonly IScriptEventManager _eventManager;
2829
private readonly IApplicationLifetime _applicationLifetime;
@@ -101,16 +102,6 @@ public Task StopAsync(CancellationToken cancellationToken)
101102
/// </summary>
102103
private void InitializeFileWatchers()
103104
{
104-
_debugModeFileWatcher = new AutoRecoveringFileSystemWatcher(_hostLogPath, ScriptConstants.DebugSentinelFileName,
105-
includeSubdirectories: false, changeTypes: WatcherChangeTypes.Created | WatcherChangeTypes.Changed);
106-
_debugModeFileWatcher.Changed += OnDebugModeFileChanged;
107-
_logger.LogDebug("Debug file watch initialized.");
108-
109-
_diagnosticModeFileWatcher = new AutoRecoveringFileSystemWatcher(_hostLogPath, ScriptConstants.DiagnosticSentinelFileName,
110-
includeSubdirectories: false, changeTypes: WatcherChangeTypes.Created | WatcherChangeTypes.Changed);
111-
_diagnosticModeFileWatcher.Changed += OnDiagnosticModeFileChanged;
112-
_logger.LogDebug("Diagnostic file watch initialized.");
113-
114105
if (_scriptOptions.FileWatchingEnabled)
115106
{
116107
_fileEventSource = new FileWatcherEventSource(_eventManager, EventSources.ScriptFiles, _scriptOptions.RootScriptPath);
@@ -126,12 +117,50 @@ private void InitializeFileWatchers()
126117
.Subscribe((msg) => ScheduleRestartAsync(false)
127118
.ContinueWith(t => _logger.LogCritical(t.Exception.Message),
128119
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously)));
120+
121+
// Delay starting up for logging and debug file watchers to avoid long start up times
122+
Utility.ExecuteAfterDelay(InitializeNonBlockingFileWatchers, TimeSpan.FromSeconds(_delayedWatchersDelaySeconds));
123+
}
124+
125+
/// <summary>
126+
/// Initializes the file and directory monitoring that does not need to happen as part of a Host startup
127+
/// These watchers can be started after a delay to avoid startup performance issue
128+
/// </summary>
129+
private void InitializeNonBlockingFileWatchers()
130+
{
131+
if (_watchersStopped)
132+
{
133+
return;
134+
}
135+
136+
lock (_stopWatchersLock)
137+
{
138+
// We need this duplication to avoid race if watchers were stopped just when we acquired the lock
139+
if (!_watchersStopped)
140+
{
141+
_debugModeFileWatcher = new AutoRecoveringFileSystemWatcher(_hostLogPath, ScriptConstants.DebugSentinelFileName,
142+
includeSubdirectories: false, changeTypes: WatcherChangeTypes.Created | WatcherChangeTypes.Changed);
143+
_debugModeFileWatcher.Changed += OnDebugModeFileChanged;
144+
_logger.LogDebug("Debug file watch initialized.");
145+
146+
_diagnosticModeFileWatcher = new AutoRecoveringFileSystemWatcher(_hostLogPath, ScriptConstants.DiagnosticSentinelFileName,
147+
includeSubdirectories: false, changeTypes: WatcherChangeTypes.Created | WatcherChangeTypes.Changed);
148+
_diagnosticModeFileWatcher.Changed += OnDiagnosticModeFileChanged;
149+
_logger.LogDebug("Diagnostic file watch initialized.");
150+
}
151+
}
129152
}
130153

131154
private void StopFileWatchers()
132155
{
156+
if (_watchersStopped)
157+
{
158+
return;
159+
}
160+
133161
lock (_stopWatchersLock)
134162
{
163+
// We need this duplication to avoid race if watchers were stopped just when we acquired the lock
135164
if (_watchersStopped)
136165
{
137166
return;

src/WebJobs.Script/Host/DebugManager.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ public void NotifyDebug()
3939
{
4040
// create or update the debug sentinel file to trigger a
4141
// debug timeout update across all instances
42-
string debugSentinelFileName = Path.Combine(_scriptOptions.CurrentValue.LogPath, "Host", ScriptConstants.DebugSentinelFileName);
42+
string debugSentinelDirectory = Path.Combine(_scriptOptions.CurrentValue.LogPath, "Host");
43+
string debugSentinelFileName = Path.Combine(debugSentinelDirectory, ScriptConstants.DebugSentinelFileName);
4344
if (!File.Exists(debugSentinelFileName))
4445
{
46+
FileUtility.EnsureDirectoryExists(debugSentinelDirectory);
4547
File.WriteAllText(debugSentinelFileName, "This is a system managed marker file used to control runtime debug mode behavior.");
4648
}
4749
else

src/WebJobs.Script/Host/ScriptHost.cs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ namespace Microsoft.Azure.WebJobs.Script
3939
public class ScriptHost : JobHost, IScriptJobHost
4040
{
4141
internal const int DebugModeTimeoutMinutes = 15;
42+
private const int FileSystemCleanupDelay = 30;
4243
private const string HostAssemblyName = "ScriptHost";
4344
private const string GeneratedTypeNamespace = "Host";
4445
internal const string GeneratedTypeName = "Functions";
@@ -47,6 +48,7 @@ public class ScriptHost : JobHost, IScriptJobHost
4748
private readonly string _storageConnectionString;
4849
private readonly IDistributedLockManager _distributedLockManager;
4950
private readonly IFunctionMetadataManager _functionMetadataManager;
51+
private readonly IFileLoggingStatusManager _fileLoggingStatusManager;
5052
private readonly IHostIdProvider _hostIdProvider;
5153
private readonly IHttpRoutesManager _httpRoutesManager;
5254
private readonly IProxyMetadataManager _proxyMetadataManager;
@@ -91,6 +93,7 @@ public ScriptHost(IOptions<JobHostOptions> options,
9193
ILoggerFactory loggerFactory,
9294
IFunctionInvocationDispatcherFactory functionDispatcherFactory,
9395
IFunctionMetadataManager functionMetadataManager,
96+
IFileLoggingStatusManager fileLoggingStatusManager,
9497
IProxyMetadataManager proxyMetadataManager,
9598
IMetricsLogger metricsLogger,
9699
IOptions<ScriptJobHostOptions> scriptHostOptions,
@@ -116,6 +119,7 @@ public ScriptHost(IOptions<JobHostOptions> options,
116119
_storageConnectionString = configuration.GetWebJobsConnectionString(ConnectionStringNames.Storage);
117120
_distributedLockManager = distributedLockManager;
118121
_functionMetadataManager = functionMetadataManager;
122+
_fileLoggingStatusManager = fileLoggingStatusManager;
119123
_applicationLifetime = applicationLifetime;
120124
_hostIdProvider = hostIdProvider;
121125
_httpRoutesManager = httpRoutesManager;
@@ -296,7 +300,7 @@ public async Task InitializeAsync(CancellationToken cancellationToken = default)
296300

297301
GenerateFunctions(directTypes);
298302

299-
CleanupFileSystem();
303+
ScheduleFileSystemCleanup();
300304
}
301305
}
302306

@@ -445,7 +449,10 @@ private void PreInitialize()
445449
/// </summary>
446450
private void InitializeFileSystem()
447451
{
448-
FileUtility.EnsureDirectoryExists(_hostLogPath);
452+
if (_fileLoggingStatusManager.IsFileLoggingEnabled)
453+
{
454+
FileUtility.EnsureDirectoryExists(_hostLogPath);
455+
}
449456

450457
if (!_environment.IsFileSystemReadOnly())
451458
{
@@ -521,14 +528,15 @@ private void AddFunctionDescriptors()
521528
/// <summary>
522529
/// Clean up any old files or directories.
523530
/// </summary>
524-
private void CleanupFileSystem()
531+
private void ScheduleFileSystemCleanup()
525532
{
526-
if (ScriptOptions.FileLoggingMode != FileLoggingMode.Never)
533+
Utility.ExecuteAfterDelay(() =>
527534
{
528-
// initiate the cleanup in a background task so we don't
529-
// delay startup
530-
Task.Run(() => PurgeOldLogDirectories());
531-
}
535+
if (ScriptOptions.FileLoggingMode != FileLoggingMode.Never)
536+
{
537+
PurgeOldLogDirectories();
538+
}
539+
}, TimeSpan.FromSeconds(FileSystemCleanupDelay));
532540
}
533541

534542
/// <summary>

src/WebJobs.Script/Utility.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,17 @@ public static bool CheckAppOffline(IEnvironment environment, string scriptPath)
681681
return false;
682682
}
683683

684+
public static void ExecuteAfterDelay(Action targetAction, TimeSpan delay, CancellationToken cancellationToken = default)
685+
{
686+
Task.Delay(delay, cancellationToken).ContinueWith(_ =>
687+
{
688+
if (!cancellationToken.IsCancellationRequested)
689+
{
690+
targetAction();
691+
}
692+
}, TaskContinuationOptions.OnlyOnRanToCompletion);
693+
}
694+
684695
public static bool TryCleanUrl(string url, out string cleaned)
685696
{
686697
cleaned = null;

test/WebJobs.Script.Tests/ScriptHostTests.cs

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,36 @@ public void LoadHostConfig_ParseError_Throws()
8989
[Fact]
9090
public static async Task OnDebugModeFileChanged_TriggeredWhenDebugFileUpdated()
9191
{
92+
var loggerProvider = new TestLoggerProvider();
93+
var loggerFactory = new LoggerFactory();
94+
loggerFactory.AddProvider(loggerProvider);
95+
9296
var host = new HostBuilder()
93-
.ConfigureDefaultTestWebScriptHost(runStartupHostedServices: true)
97+
.ConfigureDefaultTestWebScriptHost(_ => { },
98+
options =>
99+
{
100+
options.ScriptPath = Path.GetTempPath();
101+
options.LogPath = Path.GetTempPath();
102+
},
103+
runStartupHostedServices: true,
104+
rootServices =>
105+
{
106+
rootServices.AddSingleton<ILoggerFactory>(loggerFactory);
107+
})
108+
.ConfigureServices(s =>
109+
{
110+
s.AddSingleton<ILoggerFactory>(loggerFactory);
111+
})
94112
.Build();
95113

96114
ScriptHost scriptHost = host.GetScriptHost();
97-
string debugSentinelFilePath = Path.Combine(scriptHost.ScriptOptions.RootLogPath, "Host", ScriptConstants.DebugSentinelFileName);
98115

99-
// Write the initial file.
116+
string debugSentinelDirectory = Path.Combine(scriptHost.ScriptOptions.RootLogPath, "Host");
117+
string debugSentinelFilePath = Path.Combine(debugSentinelDirectory, ScriptConstants.DebugSentinelFileName);
100118
if (!File.Exists(debugSentinelFilePath))
101119
{
102-
File.WriteAllText(debugSentinelFilePath, string.Empty);
120+
FileUtility.EnsureDirectoryExists(debugSentinelDirectory);
121+
File.WriteAllText(debugSentinelFilePath, "This is a system managed marker file used to control runtime debug mode behavior.");
103122
}
104123

105124
// first put the host into a non-debug state
@@ -114,6 +133,12 @@ await TestHelpers.Await(() =>
114133
},
115134
userMessageCallback: () => $"Expected InDebugMode to be false. Now: {DateTime.UtcNow}; Sentinel LastWriteTime: {File.GetLastWriteTimeUtc(debugSentinelFilePath)}; LastDebugNotify: {debugState.LastDebugNotify}.");
116135

136+
// File Watchers for debug and diagnostic task are delayed during startup.
137+
await TestHelpers.Await(() =>
138+
{
139+
return loggerProvider.GetAllLogMessages().Any(m => m.FormattedMessage.Contains("Debug file watch initialized."));
140+
}, timeout: 30000, userMessageCallback: () => $"File watchers did not initialize in time");
141+
117142
// verify that our file watcher for the debug sentinel file is configured
118143
// properly by touching the file and ensuring that our host goes into
119144
// debug mode
@@ -173,7 +198,11 @@ public void NotifyDebug_UpdatesDebugMarkerFileAndTimestamp()
173198
var debugManager = _fixture.Host.Services.GetService<IDebugManager>();
174199

175200
string debugSentinelFileName = Path.Combine(host.ScriptOptions.RootLogPath, "Host", ScriptConstants.DebugSentinelFileName);
176-
File.Delete(debugSentinelFileName);
201+
if (File.Exists(debugSentinelFileName))
202+
{
203+
File.Delete(debugSentinelFileName);
204+
}
205+
177206
debugState.LastDebugNotify = DateTime.MinValue;
178207

179208
Assert.False(host.InDebugMode);

0 commit comments

Comments
 (0)