Skip to content
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

1388 xml serializer assembly load context awareness #58932

Conversation

StephenMolloy
Copy link
Member

Makes XmlSerializer aware of AssemblyLoadContexts

When serializing types that are in a different ALC, the serializer
should also operate in that same context. And not keep any
hard references that would prevent the context from being
unloaded.

Fixes #1388, #598, #599

@mconnew
Copy link
Member

mconnew commented Sep 10, 2021

    {

Each of the XmlMapping instances could be for a type in a different ALC. I think this is still going to break as it iterates through the types for each of the mappings and generates the serializer for them. If they are in a different ALC then type (which might be null), then it will throw. To fix this looks like it would require quite a bit of refactoring. Instead it would be worth detecting this situation and throwing an exception saying this isn't supported and all XmlMappings need to be for the same ALC.


Refers to: src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializer.cs:561 in 8146852. [](commit_id = 8146852e3ae0b9051720359291700d1e6e51a662, deletion_comment = False)

@StephenMolloy StephenMolloy force-pushed the 1388_XmlSerializer-AssemblyLoadContext-Awareness branch from e20336a to d9e96dc Compare September 28, 2021 23:15
if (!s_setMemberValueDelegateCache.TryGetValue(typeMemberNameTuple, out ReflectionXmlSerializationReaderHelper.SetMemberValueDelegate? result))
Type type = o.GetType();
var delegateCacheForType = s_setMemberValueDelegateCache.GetOrCreateValue(type);
if (!delegateCacheForType.TryGetValue(memberName, out var result))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetOrCreateValue uses Activator.CreateInstance to create an instance of TValue (ConcurrentDictionary in this case). There's another method you can use where you provide a callback which would be more efficient.

var delegateCacheForType = s_setMemberValueDelegateCache.GetValue(type, _ => return new ConcurrentDictionary<string, ReflectionXmlSerializationReaderHelper.SetMemberValueDelegate>());

Also ConcurrentDictionary is very wasteful for space in it's defaults. The default capacity is 31 and default concurrency is the number of CPU's there are. It creates the same number of locks as the concurrency and spreads the data among that number of internal data stores. The likelyhood of there being multiple threads trying to write to this concurrent dictionary is very low, and that will only happen the first time serializing a particular type. I have 2 suggestions.

  1. Use the GetValue overload above and use the ConcurrentDictionary constructor which takes concurrencyLevel and capacity. I would set capacity to 31 (seems reasonable for number of properties on a type) and concurrencyLevel to 2. It does mean if you get 3 threads trying to add to the dictionary at once, one of them would block. But that would require 3 threads trying to use a serializer for the first time each serializing the same type at once, and would only happen once.
  2. Switch to using a Hashtable and use a lock around all writes.

Also this is going to regress performance when using the reflection based serializer. I would suggest having two collections, one for the default ALC, and one for everything else. The extra cost would be on the creation, but lookup for the default case will remain exactly the same.

// Before generating any IL, check each mapping and supported type to make sure
// they are compatible with the current ALC
for (int i = 0; i < types.Length; i++)
VerifyLoadContext(types[i], mainAssembly);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the first iterator a traditional for loop and the second a foreach? Inconsistent coding style.

// they are compatible with the current ALC
for (int i = 0; i < types.Length; i++)
VerifyLoadContext(types[i], mainAssembly);
foreach (var mapping in xmlMappings)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VerifyLoadContext returns if mainAssembly is null. Could you add that check around the loop and skip it in VerifyLoadContext?


// Collectible types should be in the same collectible context
var baseALC = AssemblyLoadContext.GetLoadContext(assembly) ?? AssemblyLoadContext.CurrentContextualReflectionContext;
if (typeALC != baseALC)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is being done for every type. Can you lift this code out of the method so it's done once?

@@ -627,40 +627,48 @@ private void WriteMembers(ref object? o, Member[] members, UnknownNodeAction ele
}
}

private static readonly ConcurrentDictionary<(Type, string), ReflectionXmlSerializationReaderHelper.SetMemberValueDelegate> s_setMemberValueDelegateCache = new ConcurrentDictionary<(Type, string), ReflectionXmlSerializationReaderHelper.SetMemberValueDelegate>();
private static readonly ConditionalWeakTable<Type, Hashtable> s_setMemberValueDelegateCache = new ConditionalWeakTable<Type, Hashtable>();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are still using a ConditionalWeakTable for lookups even when using the default ALC. This will regress a lot as every single property set will hit this method and based on looking at the code for CWT, it's going to be expensive. This is used on a hot code path. You need to do the double table approach for this too.


// SxS: This method does not take any resource name and does not expose any resources to the caller.
// It's OK to suppress the SxS warning.
internal static bool IsTypeDynamic(Type type)
{
object? oIsTypeDynamic = s_tableIsTypeDynamic[type];
s_tableIsTypeDynamic.TryGetValue(type, out object? oIsTypeDynamic);
if (oIsTypeDynamic == null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confident this isn't on a hot code path for RefEmit serializer, but what about the reflection based serializer? Does this get called for every object that's being serialized or is it an initialization only usage? If the former, then this needs to do the dual table route.

{
T? ret;
AssemblyLoadContext? alc = AssemblyLoadContext.GetLoadContext(t.Assembly);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How expensive is ALC.GetLoadContext? If it's non-trivial, it might be worth always checking the default table first and only using GetLoadContext to see which table is should go in if not found in the default. If it's cheap, then this is fine.

namespace System.Xml.Serialization
{
using System;
using System.Collections;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usings should be outside of namespace

Copy link
Member

@mconnew mconnew left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@StephenMolloy StephenMolloy merged commit d2415c4 into dotnet:main Nov 5, 2021
@StephenMolloy
Copy link
Member Author

/backport to release/6.0

@github-actions
Copy link
Contributor

github-actions bot commented Nov 5, 2021

Started backporting to release/6.0: https://github.com/dotnet/runtime/actions/runs/1427474672

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

AssemblyLoadContext crash when collectible assembly use XmlSerializer
4 participants