-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathModelComputationStatsCommand.cs
More file actions
305 lines (256 loc) · 16.3 KB
/
Copy pathModelComputationStatsCommand.cs
File metadata and controls
305 lines (256 loc) · 16.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
using CommandLine;
using ShapeDiver.Newtonsoft.Json;
using ShapeDiver.SDK.GeometryBackend.DTO;
using ShapeDiver.SDK.GeometryBackend.Resources.Interfaces;
using ShapeDiver.SDK.PlatformBackend;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PDTO = ShapeDiver.SDK.PlatformBackend.DTO;
namespace DotNetSdkSampleConsoleApp.Commands
{
/// <summary>
/// Fetch data from the log of computations of a ShapeDiver model,
/// display some statistics of the Grasshopper components taking
/// most of the computation time.
/// </summary>
[Verb("model-computation-stats", isDefault: false, HelpText = "Fetch data from the log of computations of a ShapeDiver model, display some statistics.")]
class ModelComputationStatsCommand : BaseCommand, ICommand
{
[Option('i', "identifier", HelpText = "Identifier of the model (slug, model id, geometry backend model id, or ticket)", Required = false)]
public string Identifier { get; set; }
[Option('d', "days", HelpText = "Number of past days to inspect computation stats for")]
public int Days { get; set; }
public async Task Execute()
{
await WrapExceptions(async () =>
{
// use at least one day
Days = Days <= 0 ? 1 : Days;
// get identifier
if (String.IsNullOrEmpty(Identifier))
{
Console.Write("Model identifier (slug, model id, geometry backend model id): ");
Identifier = Console.ReadLine();
}
if (String.IsNullOrEmpty(Identifier))
{
throw new Exception($"Refusing to proceed without a model identifier");
}
// in case the identifier is a url, guess the slug from it
if (Identifier.StartsWith("https://"))
Identifier = Identifier.Split('/').Last();
// get authenticated SDK
var sdk = await GetAuthenticatedSDK();
// in case a ticket was given, try to decrypt it
if (Identifier.Length > 100 && Identifier.Split('-').Count() == 2 && Identifier.ToLowerInvariant() == Identifier)
{
Console.WriteLine($"Looks like a ticket, trying to decrypt it...");
var ticketDecrypted = await sdk.PlatformClient.ModelApi.DecryptTicket(Identifier);
Identifier = ticketDecrypted.Model.Id;
}
// keep a log of message and export it to a file
var log = new StringBuilder();
Action<string> logMessage = (string message) =>
{
Console.WriteLine(message);
log.AppendLine(message);
};
// get model call
logMessage($"Get model information for identifier {Identifier} ...");
var model = (await sdk.PlatformClient.ModelApi.Get<PDTO.ModelDto>(Identifier, ModelGetEmbeddableFields.Backend_System)).Data;
var token = (await sdk.PlatformClient.TokenApi.Create(model.Id, new List<PDTO.ModelTokenScopeEnum>() { PDTO.ModelTokenScopeEnum.GroupAnalytics }));
// get instance of low-level geometry backend SDK
var gbSdk = sdk.GeometryBackendClient.CreateLowLevelApi(model.BackendSystem.ModelViewUrl, token.Data.AccessToken);
// query model computations
var timestampFrom = DateTime.UtcNow.AddDays(-1.0 * Days).ToString("yyyyMMddHHmmssfff");
var timestampTo = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff");
var order = RequestModelComputationQueryOrder.Desc;
// collect computation stats for exporting a csv and json file
List<GeometryBackendModelComputationDto> computations = new List<GeometryBackendModelComputationDto>();
// keep a dictionary of components using most of the computation time
Dictionary<string, ComponentStats> Components = new Dictionary<string, ComponentStats>();
logMessage($"Fetch computations from log between {timestampFrom} and {timestampTo} ...");
var response = await gbSdk.Model.QueryComputations(model.GeometryBackendId, timestampFrom, timestampTo, order: order);
while (true)
{
if (response.Computations.Count > 0)
{
logMessage($"{response.Computations.First().Timestamp} - {response.Computations.Last().Timestamp}: {response.Computations.Count} computations");
}
computations.AddRange(response.Computations);
foreach (var computation in response.Computations)
{
var computed = computation.Stats?.Model?.Components?.Computed;
if (computed == null) continue;
foreach (var component in computed)
{
if (!Components.ContainsKey(component.Instance))
{
Components[component.Instance] = new ComponentStats() { Component = component };
}
Components[component.Instance].RegisterComputation(component.Time);
}
}
if (String.IsNullOrEmpty(response.Pagination.NextOffset))
break;
response = await gbSdk.Model.QueryComputations(model.GeometryBackendId, timestampFrom, timestampTo, order: order, offset: response.Pagination.NextOffset);
}
if (computations.Count == 0)
{
logMessage($"No computations found.");
return;
}
// timestamp of the youngest computation
var lastComputationTimestamp = computations.First().Timestamp.ToString();
var prefix = $"{model.Slug}--{lastComputationTimestamp}";
// build a csv file containing the component stats
IEnumerable<ComponentStats> componentsOrderedByAvgTimeDesc = Components.Select(c => c.Value).OrderBy(c => c.AvgTime).Reverse();
StringBuilder sb = new StringBuilder();
sb.AppendLine($"Name,InstanceId,NickName,AvgTime,MaxTime,Count,TotalTime");
foreach (var c in componentsOrderedByAvgTimeDesc)
{
sb.AppendLine($"{c.Component.Name},{c.Component.Instance},{c.Component.NickName},{N2S(c.AvgTime)},{N2S(c.MaxTime)},{c.Count},{N2S(c.TotalTime)}");
}
var componentCsv = sb.ToString();
var componentCsvFilename = $"{prefix}--components-stats.csv";
logMessage($"Exported information about {componentsOrderedByAvgTimeDesc.Count()} components sorted by decreasing average computation time to {componentCsvFilename}.");
File.WriteAllText(componentCsvFilename, componentCsv);
// csv file containing the stats for successful computations without exports
var computationsCsvFilename = $"{prefix}--computations-stats.csv";
var computationsSelected = computations.Where(c => c.Status == ModelComputationStatusEnum.Success && c.Id != c.ComputeRequestId && c.Exports.Count == 0);
ExportComputationsSummaryToCsv(computationsCsvFilename, computationsSelected);
logMessage($"Exported stats of {computationsSelected.Count()} successful computations to {computationsCsvFilename}.");
// csv file containing the stats for successful exports
var exportsCsvFilename = $"{prefix}--exports-stats.csv";
computationsSelected = computations.Where(c => c.Status == ModelComputationStatusEnum.Success && c.Id != c.ComputeRequestId && c.Exports.Count != 0);
ExportComputationsSummaryToCsv(exportsCsvFilename, computationsSelected);
logMessage($"Exported stats of {computationsSelected.Count()} successful exports to {exportsCsvFilename}.");
// csv file containing the stats for failed computations and exports
var failedCsvFilename = $"{prefix}--failed-stats.csv";
computationsSelected = computations.Where(c => c.Status != ModelComputationStatusEnum.Success && c.Id != c.ComputeRequestId);
ExportComputationsSummaryToCsv(failedCsvFilename, computationsSelected);
logMessage($"Exported stats of {computationsSelected.Count()} failed computations and exports to {failedCsvFilename}.");
// csv file containing the stats for model loading
var loadingCsvFilename = $"{prefix}--loading-stats.csv";
var modelLoadingComputations = computations.Where(c => c.Status == ModelComputationStatusEnum.Success && c.Id == c.ComputeRequestId);
ExportModelLoadingSummaryToCsv(loadingCsvFilename, modelLoadingComputations);
logMessage($"Exported stats of {modelLoadingComputations.Count()} processed model loading requests to {loadingCsvFilename}.");
// json file containing the stats for all computations
var jsonFilename = $"{prefix}--computations-data.json";
File.WriteAllText(jsonFilename, JsonConvert.SerializeObject(computations, Formatting.Indented));
logMessage($"Exported data of all computations to {jsonFilename}.");
// find and report extreme computations
logMessage($"{Environment.NewLine}Summary statistics of successful computations:");
var successfullComputations = computations.Where(c => c.Status == ModelComputationStatusEnum.Success && c.Id != c.ComputeRequestId);
PrintSummaryStatistic(successfullComputations, c => c.Stats.TimeSolver, "Milliseconds used by Grasshopper solver (time_solver)", logMessage);
PrintSummaryStatistic(successfullComputations, c => c.Stats.TimeSolverCollect, "Milliseconds used to collect data after solution (time_solver_collect)", logMessage);
PrintSummaryStatistic(successfullComputations, c => c.Stats.TimeStorage, "Milliseconds used to store data (time_storage)", logMessage);
PrintSummaryStatistic(successfullComputations, c => c.Stats.TimeProcessing, "Milliseconds used to process the request (time_processing)", logMessage);
PrintSummaryStatistic(successfullComputations, c => c.Stats.TimeWait, "Milliseconds the request waited before being processed (time_wait)", logMessage);
PrintSummaryStatistic(successfullComputations, c => c.Stats.TimeCompletion, "Milliseconds used to answer the request (time_completion)", logMessage);
PrintSummaryStatistic(successfullComputations, c => c.Stats.SizeAssets, "Size of resulting data in bytes (size_assets)", logMessage);
PrintSummaryStatistic(modelLoadingComputations, c => c.Stats.TimeModelOpen, "Milliseconds used during model loading for opening the model (time_model_open)", logMessage);
PrintSummaryStatistic(modelLoadingComputations, c => c.Stats.TimeModelPrepare, "Milliseconds used during model loading for preparation of scripted components etc (time_model_prepare)", logMessage);
File.WriteAllText($"{prefix}--log.txt", log.ToString());
});
}
void ExportComputationsSummaryToCsv(string filename, IEnumerable<GeometryBackendModelComputationDto> computations)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine($"timestamp,time_solver,time_solver_collect,time_storage,time_processing,time_wait,time_completion,size_assets,status");
foreach (var computation in computations)
{
sb.AppendLine($"{computation.Timestamp},{computation.Stats.TimeSolver},{computation.Stats.TimeSolverCollect},{computation.Stats.TimeStorage},{computation.Stats.TimeProcessing},{computation.Stats.TimeWait},{computation.Stats.TimeCompletion},{computation.Stats.SizeAssets},{computation.Status}");
}
var csv = sb.ToString();
File.WriteAllText(filename, csv);
}
void ExportModelLoadingSummaryToCsv(string filename, IEnumerable<GeometryBackendModelComputationDto> computations)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine($"timestamp,time_model_open,time_model_prepare,time_completion,status");
foreach (var computation in computations)
{
// TODO SS-9529 replace by c.Stats.TimeModelPrepare once SDK 1.32 has been released
sb.AppendLine($"{computation.Timestamp},{computation.Stats.TimeModelOpen},{computation.Stats.TimeModelPrepare},{computation.Stats.TimeCompletion},{computation.Status}");
}
var csv = sb.ToString();
File.WriteAllText(filename, csv);
}
void PrintSummaryStatistic(IEnumerable<GeometryBackendModelComputationDto> computations, Func<GeometryBackendModelComputationDto, long> selector, string description, Action<string> logMessage)
{
var values = computations.Select(c => selector(c)).ToList();
var min = values.Min();
var max = values.Max();
var avg = values.Average();
var sortedNumbers = values.OrderBy(n => n).ToList();
Func<double, string> p = (double d) => N2S(CalculatePercentile(sortedNumbers, d));
logMessage($"{description} - Min,Avg,Max: {min},{N2S(avg)},{max} - p01,p05,p10,p25,p50,p75,p90,p95,p99: {p(0.01)},{p(0.05)},{p(0.1)},{p(0.25)},{p(0.5)},{p(0.75)},{p(0.9)},{p(0.95)},{p(0.99)}");
}
string N2S(double n)
{
return n.ToString("0", System.Globalization.CultureInfo.InvariantCulture);
}
/// <summary>
/// Calculate the percentile of a sequence of numbers.
/// </summary>
/// <param name="sortedNumbers">must be ordered!</param>
/// <param name="percentile"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
static double CalculatePercentile(List<long> sortedNumbers, double percentile)
{
if (sortedNumbers == null || sortedNumbers.Count < 2)
{
throw new ArgumentException("The sequence must contain at least two elements.", nameof(sortedNumbers));
}
// Calculate the index for the percentile
double index = (percentile * (sortedNumbers.Count - 1)) + 1;
// Interpolate to get the value at the calculated index
int lowerIndex = (int)index;
int upperIndex = lowerIndex + 1;
double lowerValue = sortedNumbers[lowerIndex - 1];
double upperValue = sortedNumbers[upperIndex - 1];
double interpolatedValue = lowerValue + (index - lowerIndex) * (upperValue - lowerValue);
return interpolatedValue;
}
}
/// <summary>
/// Stats about a component of the model.
/// </summary>
class ComponentStats
{
/// <summary>
/// Information about the component.
/// </summary>
public GeometryBackendModelComputationComponentComputedDto Component { get; set; }
/// <summary>
/// Total computation time for this component (sum of all computation times).
/// </summary>
public double TotalTime { get; private set; }
/// <summary>
/// Maximum computation time for this component
/// </summary>
public double MaxTime { get; private set; }
/// <summary>
/// Number of computations for this component.
/// Note that the computation logs only record the computations of those 10 components which
/// took most computation time.
/// </summary>
public int Count { get; private set; }
/// <summary>
/// Average computation time for this component.
/// </summary>
public double AvgTime => TotalTime / Count;
public void RegisterComputation(double time)
{
TotalTime += time;
MaxTime = Math.Max(MaxTime, time);
Count++;
}
}
}