-
-
Notifications
You must be signed in to change notification settings - Fork 161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP - InteractionProfilerGrain #345
base: master
Are you sure you want to change the base?
Changes from 3 commits
c321edf
359406d
ad407b5
4992ac2
668d0f1
8347a56
b9f8836
ffd0004
11967cb
07cf595
291463e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
using System; | ||
using System.Runtime.CompilerServices; | ||
using System.Threading.Tasks; | ||
using Orleans; | ||
using Orleans.Concurrency; | ||
using OrleansDashboard.Model; | ||
|
||
namespace OrleansDashboard.Metrics | ||
{ | ||
public interface IInteractionProfiler : IGrainWithIntegerKey | ||
{ | ||
[OneWay] | ||
Task Track(GrainInteractionInfoEntry entry); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
using System; | ||
|
||
namespace OrleansDashboard.Model | ||
{ | ||
[Serializable] | ||
public class GrainInteractionInfoEntry | ||
{ | ||
public string Grain { get; set; } | ||
public string TargetGrain { get; set; } | ||
public string Method { get; set; } | ||
public uint Count { get; set; } = 1; | ||
|
||
public string Key => Grain + ":" + (TargetGrain ?? string.Empty) + ":" + Method; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
using System.Collections.Generic; | ||
using System.Threading.Tasks; | ||
using Orleans; | ||
using Orleans.Runtime; | ||
using OrleansDashboard.Metrics; | ||
using OrleansDashboard.Model; | ||
|
||
namespace OrleansDashboard.Implementation | ||
{ | ||
public sealed class GrainInteractionFilter : IIncomingGrainCallFilter, IOutgoingGrainCallFilter | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we not extend the current filter for that? Because we have some logic there that we should keep. Like custom method names and attributes to opt out from the profiler. |
||
{ | ||
private readonly IGrainFactory grainFactory; | ||
private IInteractionProfiler grainInteractionProfiler; | ||
|
||
public GrainInteractionFilter(IGrainFactory grainFactory) | ||
{ | ||
this.grainFactory = grainFactory; | ||
} | ||
|
||
public async Task Invoke(IIncomingGrainCallContext context) | ||
{ | ||
try | ||
{ | ||
var call = GetGrainMethod(context); | ||
TrackBeginInvoke(call.Grain, call.Method); | ||
await context.Invoke(); | ||
} | ||
finally | ||
{ | ||
await TrackEndInvoke(); | ||
} | ||
} | ||
|
||
public async Task Invoke(IOutgoingGrainCallContext context) | ||
{ | ||
try | ||
{ | ||
var call = GetGrainMethod(context); | ||
TrackBeginInvoke(call.Grain, call.Method); | ||
await context.Invoke(); | ||
} | ||
finally | ||
{ | ||
await TrackEndInvoke(); | ||
} | ||
} | ||
|
||
private Stack<GrainInteractionInfoEntry> GetCallStack() | ||
{ | ||
return RequestContext.Get(nameof(GrainInteractionFilter)) as Stack<GrainInteractionInfoEntry> ?? new Stack<GrainInteractionInfoEntry>(); | ||
} | ||
|
||
private void SaveCallStack(Stack<GrainInteractionInfoEntry> stack) | ||
{ | ||
if (stack.Count == 0) | ||
{ | ||
RequestContext.Remove(nameof(GrainInteractionFilter)); | ||
} | ||
else | ||
{ | ||
RequestContext.Set(nameof(GrainInteractionFilter), stack); | ||
} | ||
} | ||
|
||
private void TrackBeginInvoke(string grain, string method) | ||
{ | ||
var stack = GetCallStack(); | ||
if (stack.TryPeek(out var info)) | ||
{ | ||
info.TargetGrain = grain; | ||
} | ||
|
||
stack.Push(new GrainInteractionInfoEntry | ||
{ | ||
Grain = grain, | ||
Method = method | ||
}); | ||
SaveCallStack(stack); | ||
} | ||
|
||
private async Task TrackEndInvoke() | ||
{ | ||
var stack = GetCallStack(); | ||
var info = stack.Pop(); | ||
|
||
grainInteractionProfiler ??= grainFactory.GetGrain<IInteractionProfiler>(0); | ||
await grainInteractionProfiler.Track(info); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the current profiler we use batching to reduce the grain calls. I think we should use the same approach here. |
||
SaveCallStack(stack); | ||
} | ||
|
||
private (string Grain, string Method) GetGrainMethod(IIncomingGrainCallContext context) | ||
{ | ||
return (context.InterfaceMethod.ReflectedType.Name, context.InterfaceMethod.Name); | ||
} | ||
|
||
private (string Grain, string Method) GetGrainMethod(IOutgoingGrainCallContext context) | ||
{ | ||
return (context.InterfaceMethod.ReflectedType.Name, context.InterfaceMethod.Name); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.Options; | ||
using Orleans; | ||
using Orleans.Concurrency; | ||
using OrleansDashboard.Model; | ||
|
||
namespace OrleansDashboard.Metrics.Grains | ||
{ | ||
[Reentrant] | ||
public class InteractionProfilerGrain : Grain, IInteractionProfiler | ||
{ | ||
private const int DefaultTimerIntervalMs = 1000; // 1 second | ||
private readonly Dictionary<string, Dictionary<string, GrainInteractionInfoEntry>> interaction = new(); | ||
private readonly DashboardOptions options; | ||
private IDisposable timer; | ||
|
||
public InteractionProfilerGrain(IOptions<DashboardOptions> options) | ||
{ | ||
this.options = options.Value; | ||
} | ||
|
||
public Task Track(GrainInteractionInfoEntry entry) | ||
{ | ||
if (interaction.TryGetValue(entry.Grain, out var existing)) | ||
{ | ||
if (existing.TryGetValue(entry.Key, out var existingEntry)) | ||
{ | ||
existingEntry.Count++; | ||
} | ||
else | ||
{ | ||
existing[entry.Key] = entry; | ||
} | ||
} | ||
else | ||
{ | ||
interaction.Add(entry.Grain, new Dictionary<string, GrainInteractionInfoEntry> | ||
{ | ||
[entry.Key] = entry | ||
}); | ||
} | ||
|
||
return Task.CompletedTask; | ||
} | ||
|
||
public override async Task OnActivateAsync() | ||
{ | ||
var updateInterval = TimeSpan.FromMilliseconds(Math.Max(options.CounterUpdateIntervalMs, DefaultTimerIntervalMs)); | ||
|
||
try | ||
{ | ||
timer = RegisterTimer(x => CollectStatistics((bool)x), true, updateInterval, updateInterval); | ||
} | ||
catch (InvalidOperationException) | ||
{ | ||
Debug.WriteLine("Not running in Orleans runtime"); | ||
} | ||
|
||
await base.OnActivateAsync(); | ||
} | ||
|
||
private async Task CollectStatistics(bool canDeactivate) | ||
{ | ||
var dashboardGrain = GrainFactory.GetGrain<IDashboardGrain>(0); | ||
try | ||
{ | ||
await dashboardGrain.SubmitGrainInteraction(BuildGraph()); | ||
} | ||
catch (Exception) | ||
{ | ||
// we can't get the silo stats, it's probably dead, so kill the grain | ||
if (canDeactivate) | ||
{ | ||
timer?.Dispose(); | ||
timer = null; | ||
|
||
DeactivateOnIdle(); | ||
} | ||
} | ||
} | ||
|
||
private string BuildGraph() | ||
{ | ||
var content = string.Join("\n ", interaction.Values.SelectMany(s => s) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I would prefer not to concern the back end with front end configuration, such as colours. Could we just return pure data as JSON to the front end? |
||
/*.Where(w=>!string.IsNullOrEmpty(w.To))*/ | ||
.Select(s => $"{s.Value.Grain} -> {s.Value.TargetGrain ?? s.Value.Grain+"_self"} [ label = \"{s.Value.Method}\", color=\"0.650 0.700 0.700\" ];")); | ||
|
||
var colors = string.Join("\n", interaction.Values.SelectMany(s => s) | ||
.Select(s => s.Value.Grain) | ||
.Distinct() | ||
.Select(s => $"{s} [color=\"0.628 0.227 1.000\"];")); | ||
|
||
var graphCode = @$" | ||
digraph finite_state_machine {{ | ||
rankdir=LR; | ||
ratio = fill; | ||
node [style=filled]; | ||
{content} | ||
|
||
{colors} | ||
}}"; | ||
return graphCode; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,8 @@ public static ISiloHostBuilder UseDashboard(this ISiloHostBuilder builder, | |
{ | ||
builder.ConfigureApplicationParts(parts => parts.AddDashboardParts()); | ||
builder.ConfigureServices(services => services.AddDashboard(configurator)); | ||
builder.AddIncomingGrainCallFilter<GrainInteractionFilter>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I would like a way to control (switch on/off) this profiling, as it may get slow? |
||
builder.AddOutgoingGrainCallFilter<GrainInteractionFilter>(); | ||
builder.AddStartupTask<Dashboard>(); | ||
|
||
return builder; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we start to use records for that?