Skip to content

Commit 60c80e0

Browse files
committed
File logging perf optimizations
1 parent 5f90338 commit 60c80e0

File tree

9 files changed

+146
-43
lines changed

9 files changed

+146
-43
lines changed

src/WebJobs.Script/Description/CSharp/CSharpFunctionDescriptionProvider.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ protected override Collection<ParameterDescriptor> GetFunctionParameters(IFuncti
127127
}
128128
}
129129

130+
// Add any additional common System parameters
131+
// Add ExecutionContext to provide access to InvocationId, etc.
132+
descriptors.Add(new ParameterDescriptor("context", typeof(ExecutionContext)));
133+
130134
return descriptors;
131135
}
132136
catch (AggregateException exc)

src/WebJobs.Script/Description/CSharp/CSharpFunctionInvoker.cs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,19 +171,34 @@ private void RestorePackages()
171171

172172
public override async Task Invoke(object[] parameters)
173173
{
174+
// Separate system parameters from the actual method parameters
175+
object[] originalParameters = parameters;
176+
MethodInfo function = await GetFunctionTargetAsync();
177+
int actualParameterCount = function.GetParameters().Length;
178+
object[] systemParameters = parameters.Skip(actualParameterCount).ToArray();
179+
parameters = parameters.Take(actualParameterCount).ToArray();
180+
181+
ExecutionContext functionExecutionContext = (ExecutionContext)systemParameters[0];
182+
string invocationId = functionExecutionContext.InvocationId.ToString();
183+
174184
FunctionStartedEvent startedEvent = new FunctionStartedEvent(Metadata);
175185
_metrics.BeginEvent(startedEvent);
176186

177187
try
178188
{
179-
TraceWriter.Verbose("Function started");
189+
TraceWriter.Verbose(string.Format("Function started (Id={0})", invocationId));
180190

181191
parameters = ProcessInputParameters(parameters);
182192

183-
MethodInfo function = await GetFunctionTargetAsync();
184-
185193
object functionResult = function.Invoke(null, parameters);
186194

195+
// after the function executes, we have to copy values back into the original
196+
// array to ensure object references are maintained (since we took a copy above)
197+
for (int i = 0; i < parameters.Length; i++)
198+
{
199+
originalParameters[i] = parameters[i];
200+
}
201+
187202
if (functionResult is Task)
188203
{
189204
functionResult = await((Task)functionResult).ContinueWith(t => GetTaskResult(t));
@@ -194,7 +209,7 @@ public override async Task Invoke(object[] parameters)
194209
_resultProcessor(function, parameters, functionResult);
195210
}
196211

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

206-
TraceWriter.Verbose("Function completed (Failure)");
221+
TraceWriter.Verbose(string.Format("Function completed (Failure, Id={0})", invocationId));
207222
throw;
208223
}
209224
finally

src/WebJobs.Script/Description/NodeFunctionInvoker.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,18 +95,19 @@ public override async Task Invoke(object[] parameters)
9595
TraceWriter traceWriter = (TraceWriter)parameters[1];
9696
IBinder binder = (IBinder)parameters[2];
9797
ExecutionContext functionExecutionContext = (ExecutionContext)parameters[3];
98+
string invocationId = functionExecutionContext.InvocationId.ToString();
9899

99100
FunctionStartedEvent startedEvent = new FunctionStartedEvent(Metadata);
100101
_metrics.BeginEvent(startedEvent);
101102

102103
try
103104
{
104-
TraceWriter.Verbose(string.Format("Function started"));
105+
TraceWriter.Verbose(string.Format("Function started (Id={0})", invocationId));
105106

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

108109
Dictionary<string, string> bindingData = GetBindingData(input, binder, _inputBindings, _outputBindings);
109-
bindingData["InvocationId"] = functionExecutionContext.InvocationId.ToString();
110+
bindingData["InvocationId"] = invocationId;
110111
scriptExecutionContext["bindingData"] = bindingData;
111112

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

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

118-
TraceWriter.Verbose(string.Format("Function completed (Success)"));
119+
TraceWriter.Verbose(string.Format("Function completed (Success, Id={0})", invocationId));
119120
}
120121
catch (Exception ex)
121122
{
122123
startedEvent.Success = false;
123124
TraceWriter.Error(ex.Message, ex);
124-
TraceWriter.Verbose(string.Format("Function completed (Failure)"));
125+
TraceWriter.Verbose(string.Format("Function completed (Failure, Id={0})", invocationId));
125126
throw;
126127
}
127128
finally

src/WebJobs.Script/Description/ScriptFunctionInvoker.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ internal async Task ExecuteScriptAsync(string path, string arguments, object[] i
9090
TraceWriter traceWriter = (TraceWriter)invocationParameters[1];
9191
IBinder binder = (IBinder)invocationParameters[2];
9292
ExecutionContext functionExecutionContext = (ExecutionContext)invocationParameters[3];
93+
string invocationId = functionExecutionContext.InvocationId.ToString();
9394

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

112-
TraceWriter.Verbose(string.Format("Function started"));
113+
TraceWriter.Verbose(string.Format("Function started (Id={0})", invocationId));
113114

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

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

141141
string error = process.StandardError.ReadToEnd();
142142
TraceWriter.Error(error);
143-
TraceWriter.Verbose(string.Format("Function completed (Failure)"));
143+
TraceWriter.Verbose(string.Format("Function completed (Failure, Id={0})", invocationId));
144144
throw new ApplicationException(error);
145145
}
146146

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

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

153-
TraceWriter.Verbose(string.Format("Function completed (Success)"));
153+
TraceWriter.Verbose(string.Format("Function completed (Success, Id={0})", invocationId));
154154
}
155155

156156
private void InitializeEnvironmentVariables(Dictionary<string, string> environmentVariables, string functionInstanceOutputPath, object input, Collection<FunctionBinding> outputBindings, ExecutionContext context)

src/WebJobs.Script/Description/ScriptFunctionInvokerBase.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ protected virtual void Dispose(bool disposing)
184184
{
185185
_fileWatcher.Dispose();
186186
}
187+
188+
if (TraceWriter != null && TraceWriter is IDisposable)
189+
{
190+
((IDisposable)TraceWriter).Dispose();
191+
}
187192
}
188193

189194
_disposed = true;

src/WebJobs.Script/FileTraceWriter.cs

Lines changed: 98 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,29 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Concurrent;
56
using System.Diagnostics;
67
using System.Globalization;
78
using System.IO;
89
using System.Linq;
10+
using System.Text;
11+
using System.Timers;
912
using Microsoft.Azure.WebJobs.Host;
1013

1114
namespace Microsoft.Azure.WebJobs.Script
1215
{
13-
public class FileTraceWriter : TraceWriter
16+
public class FileTraceWriter : TraceWriter, IDisposable
1417
{
1518
private const long MaxLogFileSizeBytes = 5 * 1024 * 1024;
19+
private const int LogFlushIntervalMs = 1000;
1620
private readonly string _logFilePath;
1721
private readonly string _instanceId;
1822
private static object _syncLock = new object();
1923
private FileInfo _currentLogFileInfo;
24+
private bool _disposed = false;
25+
26+
private Timer _flushTimer;
27+
private ConcurrentQueue<string> _logBuffer = new ConcurrentQueue<string>();
2028

2129
public FileTraceWriter(string logFilePath, TraceLevel level) : base(level)
2230
{
@@ -39,6 +47,62 @@ public FileTraceWriter(string logFilePath, TraceLevel level) : base(level)
3947
{
4048
SetNewLogFile();
4149
}
50+
51+
// start a timer to flush accumulated logs in batches
52+
_flushTimer = new Timer
53+
{
54+
AutoReset = true,
55+
Interval = LogFlushIntervalMs
56+
};
57+
_flushTimer.Elapsed += OnFlushLogs;
58+
_flushTimer.Start();
59+
}
60+
61+
public void FlushToFile()
62+
{
63+
if (_logBuffer.Count == 0)
64+
{
65+
return;
66+
}
67+
68+
// snapshot the current set of buffered logs
69+
// and set a new bag
70+
ConcurrentQueue<string> currentBuffer = _logBuffer;
71+
_logBuffer = new ConcurrentQueue<string>();
72+
73+
// concatenate all lines into one string
74+
StringBuilder sb = new StringBuilder();
75+
string line = null;
76+
while (currentBuffer.TryDequeue(out line))
77+
{
78+
sb.AppendLine(line);
79+
}
80+
81+
// write all lines in a single file operation
82+
string contents = sb.ToString();
83+
try
84+
{
85+
lock (_syncLock)
86+
{
87+
File.AppendAllText(_currentLogFileInfo.FullName, contents);
88+
}
89+
}
90+
catch (DirectoryNotFoundException)
91+
{
92+
// need to handle cases where log file directories might
93+
// have been deleted from underneath us
94+
Directory.CreateDirectory(_logFilePath);
95+
lock (_syncLock)
96+
{
97+
File.AppendAllText(_currentLogFileInfo.FullName, contents);
98+
}
99+
}
100+
101+
_currentLogFileInfo.Refresh();
102+
if (_currentLogFileInfo.Length > MaxLogFileSizeBytes)
103+
{
104+
SetNewLogFile();
105+
}
42106
}
43107

44108
public override void Trace(TraceEvent traceEvent)
@@ -72,42 +136,47 @@ public override void Trace(TraceEvent traceEvent)
72136
}
73137
}
74138

75-
protected virtual void AppendLine(string line)
139+
protected virtual void Dispose(bool disposing)
76140
{
77-
if (line == null)
78-
{
79-
return;
80-
}
81-
82-
// TODO: figure out the right log file format
83-
line = string.Format(CultureInfo.InvariantCulture, "{0} {1}\r\n", DateTime.Now.ToString("s", CultureInfo.InvariantCulture), line.Trim());
84-
85-
// TODO: optimize this locking
86-
try
87-
{
88-
lock (_syncLock)
89-
{
90-
File.AppendAllText(_currentLogFileInfo.FullName, line);
91-
}
92-
}
93-
catch (DirectoryNotFoundException)
141+
if (!_disposed)
94142
{
95-
// need to handle cases where log file directories might
96-
// have been deleted from underneath us
97-
Directory.CreateDirectory(_logFilePath);
98-
lock (_syncLock)
143+
if (disposing)
99144
{
100-
File.AppendAllText(_currentLogFileInfo.FullName, line);
145+
if (_flushTimer != null)
146+
{
147+
_flushTimer.Dispose();
148+
}
149+
150+
// ensure any remaining logs are flushed
151+
FlushToFile();
101152
}
153+
154+
_disposed = true;
102155
}
156+
}
103157

104-
// TODO: Need to optimize this, so we only do the check every
105-
// so often
106-
_currentLogFileInfo.Refresh();
107-
if (_currentLogFileInfo.Length > MaxLogFileSizeBytes)
158+
public void Dispose()
159+
{
160+
Dispose(true);
161+
GC.SuppressFinalize(this);
162+
}
163+
164+
protected virtual void AppendLine(string line)
165+
{
166+
if (line == null)
108167
{
109-
SetNewLogFile();
168+
return;
110169
}
170+
171+
// add the line to the current buffer batch, which is flushed
172+
// on a timer
173+
line = string.Format(CultureInfo.InvariantCulture, "{0} {1}", DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture), line.Trim());
174+
_logBuffer.Enqueue(line);
175+
}
176+
177+
private void OnFlushLogs(object sender, ElapsedEventArgs e)
178+
{
179+
FlushToFile();
111180
}
112181

113182
private void SetNewLogFile()

src/WebJobs.Script/GlobalSuppressions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,6 @@
5151
[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>)")]
5252
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration.#Active")]
5353
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration.#Functions")]
54-
[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>)")]
54+
[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>)")]
55+
[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.")]
56+
[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.")]

src/WebJobs.Script/Host/ScriptHost.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,11 @@ protected override void Dispose(bool disposing)
644644
}
645645

646646
_restartEvent.Dispose();
647+
648+
if (TraceWriter != null && TraceWriter is IDisposable)
649+
{
650+
((IDisposable)TraceWriter).Dispose();
651+
}
647652
}
648653
}
649654
}

test/WebJobs.Script.Tests/FileTraceWriterTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ private void WriteLogs(string logFilePath, int numLogs)
5454
{
5555
traceWriter.Verbose(string.Format("Test message {0} {1}", Thread.CurrentThread.ManagedThreadId, i));
5656
}
57+
58+
traceWriter.FlushToFile();
5759
}
5860
}
5961
}

0 commit comments

Comments
 (0)