firely-net-sdk icon indicating copy to clipboard operation
firely-net-sdk copied to clipboard

Add new factory method for ModelInspector that accepts predefined ClassMapping/EnumMapping/PropertyMapping to skip reflection/assembly scanning overhead

Open almostchristian opened this issue 8 months ago • 10 comments

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 Screenshot 2024-05-27 235203

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

almostchristian avatar May 29 '24 10:05 almostchristian