firely-net-sdk
firely-net-sdk copied to clipboard
Add new factory method for ModelInspector that accepts predefined ClassMapping/EnumMapping/PropertyMapping to skip reflection/assembly scanning overhead
Our company is exploring on using our FHIR web api framework inside an AWS Lambda function, but after investigating the slowness in our cold starts, we found that 40% of the cold start duration was spent on the initializing ModelInfo.ModelInspector
.
Flame graph below
Most of the time is spent doing assembly scanning for types, nested types and recursively scanning referenced assemblies., there should be a new constructor for ModelInspector
that will create a fully configured ModelInspector without assembly scanning and type scanning for nested types/enums. As further enhancement, we can add a source generator that generates code that calls this new constructor instead of ModelInspector.ForAssembly(typeof(ModelInfo).GetTypeInfo().Assembly)
.
Below is the lambda code I used to generate the above graph:
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.ForFhir());
builder.Services.AddSingleton(sp => new CapabilityStatement { Kind = CapabilityStatementKind.Capability });
builder.Services.AddAWSLambdaHosting(LambdaEventSource.HttpApi);
var app = builder.Build();
app .UseRouting()
.UseEndpoints(endpoints =>
{
endpoints.MapPost("/Appointment", (Appointment appt) => appt);
endpoints.MapGet("/metadata", ([FromServices] CapabilityStatement cap) => cap);
});
await app.RunAsync();
Proposed API changes
ModelInspector
public static ModelInspector ForPredefinedMappings(string fhirVersion, IEnumerable<ClassMapping> classMappings, IEnumerable<EnumMapping> enumMappings);
ClassMapping
Also, properties in ClassMapping, EnumMapping and PropertyMapping will be updated to be required init
instead of private setters or get only properties.
The constructor for ClassMapping will have a propertyMappingFactory argument.
public ClassMapping(Func<ClassMapping, PropertyMapping[]>? propertyMappingFactory = null)
: this(propertyMapFactory != null ? cm => new PropertyMappingCollection(propertyMappingFactory(cm)): inspectProperties)
{
}
private ClassMapping(Func<ClassMapping, PropertyMappingCollection> propertyMappingFactory)
{
_propertyMappingFactory = propertyMappingFactory;
}
EnumMapping
There will be a new constructor for EnumMapping that accepts a memberMappingFactory so that reflection can be skipped.
public EnumMapping(string? defaultCodeSystem)
{
_mappings = new(() => mappingInitializer(defaultCodeSystem));
}
public EnumMapping(Func<IReadOnlyDictionary<string, EnumMemberMapping>> memberMappingFactory)
{
_mappings = new(valueFactory: memberMappingFactory);
}
PropertyMapping
The constructor for PropertyMapping
will accept getter and setter arguments. When these are missing, they will be generated from the NativeProperty property.
private PropertyMapping(PropertyInfo nativeProperty)
{
_nativeProperty = nativeProperty;
}
public PropertyMapping(
Func<object, object?> getter,
Action<object, object?> setter)
{
_getter = getter;
_setter = setter;
}
private PropertyInfo? _nativeProperty;
public PropertyInfo NativeProperty => _nativeProperty ?? LazyInitializer.EnsureInitialized(
ref _nativeProperty,
() => Array.Find(ImplementingType.GetProperties(BindingFlags.Public | BindingFlags.Instance), x => x.GetCustomAttribute<FhirElementAttribute>()?.Name == Name)!)!;
public object? GetValue(object instance) => ensureGetter()(instance);
private Func<object, object?>? _getter;
private Func<object, object?> ensureGetter()
=> _getter ?? LazyInitializer.EnsureInitialized(ref _getter, NativeProperty.GetValueGetter)!;
public void SetValue(object instance, object? value) => ensureSetter()(instance, value);
private Action<object, object?>? _setter;
private Action<object, object?> ensureSetter()
=> _setter ?? LazyInitializer.EnsureInitialized(ref _setter, () => NativeProperty.GetValueSetter())!;
Benchmarks
Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|---|
ScanAssemblies | 10,201.36 μs | 167.577 μs | 139.934 μs | 1.000 | 0.00 | 375.0000 | 140.6250 | 3130.62 KB | 1.00 |
ImportTypeAllResources | 7,651.36 μs | 150.712 μs | 263.961 μs | 0.735 | 0.02 | 343.7500 | 156.2500 | 2924.1 KB | 0.93 |
SourceGenMappingsAllResources | 241.15 μs | 4.638 μs | 4.763 μs | 0.024 | 0.00 | 70.8008 | 27.3438 | 578.79 KB | 0.18 |
ImportType4Resources | 1,298.84 μs | 22.972 μs | 36.436 μs | 0.126 | 0.00 | 66.4063 | 17.5781 | 542.83 KB | 0.17 |
SourceGenMappings4Resources | 39.77 μs | 0.785 μs | 0.806 μs | 0.004 | 0.00 | 10.6201 | 1.7090 | 86.95 KB | 0.03 |
*updated proposed new api and benchmark numbers