Open almostchristian opened 4 months ago
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 will make this possible in the SDK.
Does ReadOnlySpan<Type>
make a difference here? We don't use that anywhere else in the SDK, but if this makes a difference we can use it here.
We should also document how to use this.
I don't think ReadOnlySpan<>
really helps since you do this only once, only a minor reduction in allocation. Maybe just sticking with IEnumerable<T>
should be enough.
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 ClassMapping
and EnumMapping
and I get the results below:
Method | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated |
---|---|---|---|---|---|---|---|
ScanAssemblies | 11,000.4 μs | 520.96 μs | 1,536.05 μs | 11,137.1 μs | 375.0000 | 218.7500 | 3061.86 KB |
ImportTypeAllResources | 7,208.8 μs | 195.20 μs | 556.92 μs | 7,054.3 μs | 328.1250 | 171.8750 | 2797.81 KB |
ImportType4Resources | 155.6 μs | 2.99 μs | 3.67 μs | 155.0 μs | 12.6953 | 1.7090 | 104.98 KB |
NewWithSourceGenMappings | 171.0 μs | 2.96 μs | 6.05 μs | 169.8 μs | 46.1426 | 15.3809 | 376.91 KB |
NewWithTypesAllResources | 4,208.6 μs | 84.09 μs | 220.05 μs | 4,159.2 μs | 273.4375 | 140.6250 | 2277.65 KB |
NewWithTypes4Resources | 132.1 μs | 2.61 μs | 4.29 μs | 131.2 μs | 10.4980 | 0.4883 | 85.77 KB |
The SourceGenMappings gives a very hefty performance improvement (171us
vs 4208.6us
)
Some issues I found in the current API is that ClassMapping
and EnumMapping
have private constructors and private property setters that make it difficult for any source generator to work with.
Please try out my branch to see if this is something the team is interested in integrating
@mmsmits
I've refined the new API that would be helpful for source generating the ModelInspector below:
Proposed new API
public 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);
}
public 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;
}
public static EnumMapping Build(string name, string? canonical, Type nativeType, FhirRelease release, string? defaultCodeSystem = null)
=> new EnumMapping(name, canonical, nativeType, release, defaultCodeSystem);
I created a source generator that generates the ClassMappings and EnumMappings and was able to achieve the numbers below:
Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
---|---|---|---|---|---|---|
ScanAssemblies | 8,240.16 μs | 163.741 μs | 212.909 μs | 375.0000 | 140.6250 | 3081.86 KB |
ImportTypeAllResources | 6,619.96 μs | 126.732 μs | 296.233 μs | 343.7500 | 156.2500 | 2909.46 KB |
SourceGenMappingsAllResources | 161.95 μs | 3.177 μs | 6.562 μs | 47.8516 | 14.8926 | 392.86 KB |
NewWithTypesAllResources | 3,972.23 μs | 77.484 μs | 64.703 μs | 281.2500 | 117.1875 | 2304.15 KB |
ImportType4Resources | 858.91 μs | 16.936 μs | 35.352 μs | 64.4531 | 15.6250 | 535.49 KB |
SourceGenMappings4Resources | 28.14 μs | 0.537 μs | 0.503 μs | 7.3242 | 0.7935 | 60.01 KB |
NewWithTypes4Resources | 352.53 μs | 6.576 μs | 12.511 μs | 28.3203 | 3.9063 | 235.47 KB |
Generated ModelInspector https://gist.github.com/almostchristian/81a18cfa62816213e9d3133ee2b0cf7e
https://github.com/almostchristian/firely-net-sdk/tree/feature/model-inspector-sourcegen
Wow, thanks for this! We will definitely take a good look at 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:
Make them more useable for a code generator.
Allow me to suggest some modifications:
ForTypes()
overload, at least, it's not used in the generated code (https://gist.github.com/almostchristian/81a18cfa62816213e9d3133ee2b0cf7e) - do we still need it?public ForPredefinedMappings(string version, List<ClassMapping> classMappings, List<EnumMapping> enumMappings)
?Build()
method. This code was from before we had the required
keyword, so I needed a constructor with the "required" properties + the rest was initializable. As far as I am concerned, we can now have a parameterless constructor + make the properties in the original constructor required.Agree? Suggestions?
The ForTypes
method is no longer necessary since using it will inevitably require reflection. I agree with using the ForPredefinedMappings
factory method.
For ClassMapping and PropertyMapping, yes I agree to make the private set
properties be required init
so we can remove the need for the Build
factory methods.
However PropertyMapping has some tricky issues:
Release
is a public readonly field, and changing it to an required init
property which may break compatibility.NativeProperty
, which is also a public readonly field, is not required to be set and is only required to generate the getter and setter implementation. Since we can generate the getter and setter via source gen, we can remove the need for this field and in my test source generator, this is always null. We can add a function to retrieve the PropertyInfo for backward compatibility but then we'd have to save the actual property name so it can be retrieved later.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 required init
properties instead of get only properties. Also, EnumMapping will have a constructor that accepts a memberMapping factory.
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