Skip to content

Commit

Permalink
File logging perf optimizations
Browse files Browse the repository at this point in the history
  • Loading branch information
mathewc committed Mar 17, 2016
1 parent 5f90338 commit 60c80e0
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ protected override Collection<ParameterDescriptor> GetFunctionParameters(IFuncti
}
}

// Add any additional common System parameters
// Add ExecutionContext to provide access to InvocationId, etc.
descriptors.Add(new ParameterDescriptor("context", typeof(ExecutionContext)));

return descriptors;
}
catch (AggregateException exc)
Expand Down
25 changes: 20 additions & 5 deletions src/WebJobs.Script/Description/CSharp/CSharpFunctionInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,19 +171,34 @@ private void RestorePackages()

public override async Task Invoke(object[] parameters)
{
// Separate system parameters from the actual method parameters
object[] originalParameters = parameters;
MethodInfo function = await GetFunctionTargetAsync();
int actualParameterCount = function.GetParameters().Length;
object[] systemParameters = parameters.Skip(actualParameterCount).ToArray();
parameters = parameters.Take(actualParameterCount).ToArray();

ExecutionContext functionExecutionContext = (ExecutionContext)systemParameters[0];
string invocationId = functionExecutionContext.InvocationId.ToString();

FunctionStartedEvent startedEvent = new FunctionStartedEvent(Metadata);
_metrics.BeginEvent(startedEvent);

try
{
TraceWriter.Verbose("Function started");
TraceWriter.Verbose(string.Format("Function started (Id={0})", invocationId));

parameters = ProcessInputParameters(parameters);

MethodInfo function = await GetFunctionTargetAsync();

object functionResult = function.Invoke(null, parameters);

// after the function executes, we have to copy values back into the original
// array to ensure object references are maintained (since we took a copy above)
for (int i = 0; i < parameters.Length; i++)
{
originalParameters[i] = parameters[i];
}

if (functionResult is Task)
{
functionResult = await((Task)functionResult).ContinueWith(t => GetTaskResult(t));
Expand All @@ -194,7 +209,7 @@ public override async Task Invoke(object[] parameters)
_resultProcessor(function, parameters, functionResult);
}

TraceWriter.Verbose("Function completed (Success)");
TraceWriter.Verbose(string.Format("Function completed (Success, Id={0})", invocationId));
}
catch (Exception ex)
{
Expand All @@ -203,7 +218,7 @@ public override async Task Invoke(object[] parameters)
startedEvent.Success = false;
TraceWriter.Error(ex.Message, ex);

TraceWriter.Verbose("Function completed (Failure)");
TraceWriter.Verbose(string.Format("Function completed (Failure, Id={0})", invocationId));
throw;
}
finally
Expand Down
9 changes: 5 additions & 4 deletions src/WebJobs.Script/Description/NodeFunctionInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,19 @@ public override async Task Invoke(object[] parameters)
TraceWriter traceWriter = (TraceWriter)parameters[1];
IBinder binder = (IBinder)parameters[2];
ExecutionContext functionExecutionContext = (ExecutionContext)parameters[3];
string invocationId = functionExecutionContext.InvocationId.ToString();

FunctionStartedEvent startedEvent = new FunctionStartedEvent(Metadata);
_metrics.BeginEvent(startedEvent);

try
{
TraceWriter.Verbose(string.Format("Function started"));
TraceWriter.Verbose(string.Format("Function started (Id={0})", invocationId));

var scriptExecutionContext = CreateScriptExecutionContext(input, traceWriter, TraceWriter, binder, functionExecutionContext);

Dictionary<string, string> bindingData = GetBindingData(input, binder, _inputBindings, _outputBindings);
bindingData["InvocationId"] = functionExecutionContext.InvocationId.ToString();
bindingData["InvocationId"] = invocationId;
scriptExecutionContext["bindingData"] = bindingData;

await ProcessInputBindingsAsync(binder, scriptExecutionContext, bindingData);
Expand All @@ -115,13 +116,13 @@ public override async Task Invoke(object[] parameters)

await ProcessOutputBindingsAsync(_outputBindings, input, binder, bindingData, scriptExecutionContext, functionResult);

TraceWriter.Verbose(string.Format("Function completed (Success)"));
TraceWriter.Verbose(string.Format("Function completed (Success, Id={0})", invocationId));
}
catch (Exception ex)
{
startedEvent.Success = false;
TraceWriter.Error(ex.Message, ex);
TraceWriter.Verbose(string.Format("Function completed (Failure)"));
TraceWriter.Verbose(string.Format("Function completed (Failure, Id={0})", invocationId));
throw;
}
finally
Expand Down
8 changes: 4 additions & 4 deletions src/WebJobs.Script/Description/ScriptFunctionInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ internal async Task ExecuteScriptAsync(string path, string arguments, object[] i
TraceWriter traceWriter = (TraceWriter)invocationParameters[1];
IBinder binder = (IBinder)invocationParameters[2];
ExecutionContext functionExecutionContext = (ExecutionContext)invocationParameters[3];
string invocationId = functionExecutionContext.InvocationId.ToString();

FunctionStartedEvent startedEvent = new FunctionStartedEvent(Metadata);
_metrics.BeginEvent(startedEvent);
Expand All @@ -109,9 +110,8 @@ internal async Task ExecuteScriptAsync(string path, string arguments, object[] i
}
}

TraceWriter.Verbose(string.Format("Function started"));
TraceWriter.Verbose(string.Format("Function started (Id={0})", invocationId));

string invocationId = functionExecutionContext.InvocationId.ToString();
string workingDirectory = Path.GetDirectoryName(_scriptFilePath);
string functionInstanceOutputPath = Path.Combine(Path.GetTempPath(), "Functions", "Binding", invocationId);

Expand Down Expand Up @@ -140,7 +140,7 @@ internal async Task ExecuteScriptAsync(string path, string arguments, object[] i

string error = process.StandardError.ReadToEnd();
TraceWriter.Error(error);
TraceWriter.Verbose(string.Format("Function completed (Failure)"));
TraceWriter.Verbose(string.Format("Function completed (Failure, Id={0})", invocationId));
throw new ApplicationException(error);
}

Expand All @@ -150,7 +150,7 @@ internal async Task ExecuteScriptAsync(string path, string arguments, object[] i

await ProcessOutputBindingsAsync(functionInstanceOutputPath, _outputBindings, input, binder, bindingData);

TraceWriter.Verbose(string.Format("Function completed (Success)"));
TraceWriter.Verbose(string.Format("Function completed (Success, Id={0})", invocationId));
}

private void InitializeEnvironmentVariables(Dictionary<string, string> environmentVariables, string functionInstanceOutputPath, object input, Collection<FunctionBinding> outputBindings, ExecutionContext context)
Expand Down
5 changes: 5 additions & 0 deletions src/WebJobs.Script/Description/ScriptFunctionInvokerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ protected virtual void Dispose(bool disposing)
{
_fileWatcher.Dispose();
}

if (TraceWriter != null && TraceWriter is IDisposable)
{
((IDisposable)TraceWriter).Dispose();
}
}

_disposed = true;
Expand Down
127 changes: 98 additions & 29 deletions src/WebJobs.Script/FileTraceWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,29 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Timers;
using Microsoft.Azure.WebJobs.Host;

namespace Microsoft.Azure.WebJobs.Script
{
public class FileTraceWriter : TraceWriter
public class FileTraceWriter : TraceWriter, IDisposable
{
private const long MaxLogFileSizeBytes = 5 * 1024 * 1024;
private const int LogFlushIntervalMs = 1000;
private readonly string _logFilePath;
private readonly string _instanceId;
private static object _syncLock = new object();
private FileInfo _currentLogFileInfo;
private bool _disposed = false;

private Timer _flushTimer;
private ConcurrentQueue<string> _logBuffer = new ConcurrentQueue<string>();

public FileTraceWriter(string logFilePath, TraceLevel level) : base(level)
{
Expand All @@ -39,6 +47,62 @@ public FileTraceWriter(string logFilePath, TraceLevel level) : base(level)
{
SetNewLogFile();
}

// start a timer to flush accumulated logs in batches
_flushTimer = new Timer
{
AutoReset = true,
Interval = LogFlushIntervalMs
};
_flushTimer.Elapsed += OnFlushLogs;
_flushTimer.Start();
}

public void FlushToFile()
{
if (_logBuffer.Count == 0)
{
return;
}

// snapshot the current set of buffered logs
// and set a new bag
ConcurrentQueue<string> currentBuffer = _logBuffer;
_logBuffer = new ConcurrentQueue<string>();

// concatenate all lines into one string
StringBuilder sb = new StringBuilder();
string line = null;
while (currentBuffer.TryDequeue(out line))
{
sb.AppendLine(line);
}

// write all lines in a single file operation
string contents = sb.ToString();
try
{
lock (_syncLock)
{
File.AppendAllText(_currentLogFileInfo.FullName, contents);
}
}
catch (DirectoryNotFoundException)
{
// need to handle cases where log file directories might
// have been deleted from underneath us
Directory.CreateDirectory(_logFilePath);
lock (_syncLock)
{
File.AppendAllText(_currentLogFileInfo.FullName, contents);
}
}

_currentLogFileInfo.Refresh();
if (_currentLogFileInfo.Length > MaxLogFileSizeBytes)
{
SetNewLogFile();
}
}

public override void Trace(TraceEvent traceEvent)
Expand Down Expand Up @@ -72,42 +136,47 @@ public override void Trace(TraceEvent traceEvent)
}
}

protected virtual void AppendLine(string line)
protected virtual void Dispose(bool disposing)
{
if (line == null)
{
return;
}

// TODO: figure out the right log file format
line = string.Format(CultureInfo.InvariantCulture, "{0} {1}\r\n", DateTime.Now.ToString("s", CultureInfo.InvariantCulture), line.Trim());

// TODO: optimize this locking
try
{
lock (_syncLock)
{
File.AppendAllText(_currentLogFileInfo.FullName, line);
}
}
catch (DirectoryNotFoundException)
if (!_disposed)
{
// need to handle cases where log file directories might
// have been deleted from underneath us
Directory.CreateDirectory(_logFilePath);
lock (_syncLock)
if (disposing)
{
File.AppendAllText(_currentLogFileInfo.FullName, line);
if (_flushTimer != null)
{
_flushTimer.Dispose();
}

// ensure any remaining logs are flushed
FlushToFile();
}

_disposed = true;
}
}

// TODO: Need to optimize this, so we only do the check every
// so often
_currentLogFileInfo.Refresh();
if (_currentLogFileInfo.Length > MaxLogFileSizeBytes)
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void AppendLine(string line)
{
if (line == null)
{
SetNewLogFile();
return;
}

// add the line to the current buffer batch, which is flushed
// on a timer
line = string.Format(CultureInfo.InvariantCulture, "{0} {1}", DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture), line.Trim());
_logBuffer.Enqueue(line);
}

private void OnFlushLogs(object sender, ElapsedEventArgs e)
{
FlushToFile();
}

private void SetNewLogFile()
Expand Down
4 changes: 3 additions & 1 deletion src/WebJobs.Script/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,6 @@
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Binding.HttpBinding.#AddResponseHeader(System.Net.Http.HttpResponseMessage,System.Collections.Generic.KeyValuePair`2<System.String,Newtonsoft.Json.Linq.JToken>)")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration.#Active")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration.#Functions")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Description.FunctionValueLoader.#Create(System.Func`2<System.Threading.CancellationToken,System.Reflection.MethodInfo>)")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Description.FunctionValueLoader.#Create(System.Func`2<System.Threading.CancellationToken,System.Reflection.MethodInfo>)")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.FileTraceWriter.#.ctor(System.String,System.Diagnostics.TraceLevel)", Justification = "Disposed in IDisposable implementation.")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Description.ScriptFunctionInvokerBase.#CreateTraceWriter(Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration,System.String)", Justification = "Disposed in IDisposable implementation.")]
5 changes: 5 additions & 0 deletions src/WebJobs.Script/Host/ScriptHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,11 @@ protected override void Dispose(bool disposing)
}

_restartEvent.Dispose();

if (TraceWriter != null && TraceWriter is IDisposable)
{
((IDisposable)TraceWriter).Dispose();
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions test/WebJobs.Script.Tests/FileTraceWriterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ private void WriteLogs(string logFilePath, int numLogs)
{
traceWriter.Verbose(string.Format("Test message {0} {1}", Thread.CurrentThread.ManagedThreadId, i));
}

traceWriter.FlushToFile();
}
}
}

0 comments on commit 60c80e0

Please sign in to comment.