-
Notifications
You must be signed in to change notification settings - Fork 345
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
Add new factory method for ModelInspector that accepts predefined ClassMapping/EnumMapping/PropertyMapping to skip reflection/assembly scanning overhead #2794
Comments
Yes, our current setup is a bit skewed towards preferring upfront setup cost (like what you would want in a server or a client). With this new solution you would have to know the types in advance - is that something you can guarantee in your case? |
Thanks for this! We should also document how to use this. |
I don't think To guarantee all the types are present, I think using source generation would be the best approach. I tried my hand creating a source generator for this and it seems to work well. |
Also in the future, we can create a source generator that creates the ClassMapping and EnumMapping removing the need for reflection at runtime. |
I've improved my source generator to generate the
The SourceGenMappings gives a very hefty performance improvement ( Some issues I found in the current API is that Please try out my branch to see if this is something the team is interested in integrating |
I've refined the new API that would be helpful for source generating the ModelInspector below: Proposed new API ModelInspectorpublic ModelInspector(string fhirVersion, IEnumerable<ClassMapping> classMappings, IEnumerable<EnumMapping> enumMappings)
{
_classMappings = new ClassMappingCollection(classMappings);
_enumMappings = new EnumMappingCollection(enumMappings);
FhirVersion = fhirVersion;
FhirRelease = FhirReleaseParser.Parse(fhirVersion);
}
public static ModelInspector ForTypes(string version, ReadOnlySpan<Type> types)
{
var fhirRelease = FhirReleaseParser.Parse(version);
var classMappings = new List<ClassMapping>(types.Length);
var enumMappings = new List<EnumMapping>(types.Length);
foreach (var type in types)
{
if (!type.IsEnum && ClassMapping.TryCreate(type, out var classMapping, fhirRelease))
{
classMappings.Add(classMapping);
}
else if (type.IsEnum && EnumMapping.TryCreate(type, out var enumMapping, fhirRelease))
{
enumMappings.Add(enumMapping);
}
}
return new ModelInspector(version, classMappings, enumMappings);
} ClassMappingpublic static ClassMapping Build(
string name,
Type nativeType,
FhirRelease release,
bool isResource = false,
bool isCodeOfT = false,
bool isFhirPrimitive = false,
bool isPrimitive = false,
bool isBackboneType = false,
string? definitionPath = null,
bool isBindable = false,
string? canonical = null,
ValidationAttribute[]? validationAttributes = null,
Func<ClassMapping, PropertyMapping[]>? propertyMapFactory = null)
{
Func<ClassMapping, PropertyMappingCollection>? propMappingFactory = null;
if (propertyMapFactory != null)
{
propMappingFactory = cm => new PropertyMappingCollection(propertyMapFactory(cm));
}
var mapping = new ClassMapping(name, nativeType, release, propMappingFactory)
{
IsResource = isResource,
IsCodeOfT = isCodeOfT,
IsFhirPrimitive = isFhirPrimitive,
IsPrimitive = isPrimitive,
IsBackboneType = isBackboneType,
DefinitionPath = definitionPath,
IsBindable = isBindable,
Canonical = canonical,
ValidationAttributes = validationAttributes ?? [],
};
_mappedClasses.GetOrAdd((nativeType, release), mapping);
return mapping;
}
private ClassMapping(string name, Type nativeType, FhirRelease release, Func<ClassMapping, PropertyMappingCollection>? propertyMappingFactory = null)
{
Name = name;
NativeType = nativeType;
Release = release;
_propertyMappingFactory = propertyMappingFactory ?? inspectProperties;
} EnumMapping
I created a source generator that generates the ClassMappings and EnumMappings and was able to achieve the numbers below:
Generated ModelInspector https://github.com/almostchristian/firely-net-sdk/tree/feature/model-inspector-sourcegen |
Wow, thanks for this! |
Ok, I think we should do this. Our goal for a new PR would be to change the ClassMapping/PropertyMapping/ModelInspector in a way that we:
Allow me to suggest some modifications: ModelInspector API changes
ClassMapping
PropertyMapping
Agree? Suggestions? |
The For ClassMapping and PropertyMapping, yes I agree to make the However PropertyMapping has some tricky issues:
public class PropertyMapping
{
private PropertyMapping(PropertyInfo nativeProperty)
{
_nativeProperty = nativeProperty;
}
public PropertyMapping(
Func<object, object?> getter,
Action<object, object?> setter)
{
_getter = getter;
_setter = setter;
}
public PropertyInfo NativeProperty => _nativeProperty ?? LazyInitializer.EnsureInitialized(
ref _nativeProperty,
() => ImplementingType.GetProperties(BindingFlags.Public | BindingFlags.Instance).First(x => x.GetCustomAttribute<FhirElementAttribute>()?.Name == Name)!)!;
private Func<object, object?> EnsureGetter()
=> _getter ?? LazyInitializer.EnsureInitialized(ref _getter, NativeProperty.GetValueGetter)!;
private Action<object, object?> EnsureSetter()
=> _setter ?? LazyInitializer.EnsureInitialized(ref _setter, () => NativeProperty.GetValueSetter())!;
} Also, EnumMapping and EnumMemberMapping also needs to be updated to be similar to ClassMapping in that the properties should be updated to be public EnumMapping(string? defaultCodeSystem)
{
_mappings = new(() => mappingInitializer(defaultCodeSystem));
}
public EnumMapping(Func<IReadOnlyDictionary<string, EnumMemberMapping>> memberMappingFactory)
{
_mappings = new(valueFactory: memberMappingFactory);
} |
I have further (breaking) improvements to make, so I am now planning to integrate this PR in the SDK 6.0 |
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 ofModelInspector.ForAssembly(typeof(ModelInfo).GetTypeInfo().Assembly)
.Below is the lambda code I used to generate the above graph:
Proposed API changes
ModelInspector
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.
EnumMapping
There will be a new constructor for EnumMapping that accepts a memberMappingFactory so that reflection can be skipped.
PropertyMapping
The constructor for
PropertyMapping
will accept getter and setter arguments. When these are missing, they will be generated from the NativeProperty property.Benchmarks
*updated proposed new api and benchmark numbers
The text was updated successfully, but these errors were encountered: