diff --git a/lang/csharp/src/apache/benchmark/ObjectCreatorBenchmarks.cs b/lang/csharp/src/apache/benchmark/ObjectCreatorBenchmarks.cs new file mode 100644 index 00000000000..656b0865cdf --- /dev/null +++ b/lang/csharp/src/apache/benchmark/ObjectCreatorBenchmarks.cs @@ -0,0 +1,118 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using Avro.Specific; +using BenchmarkDotNet.Attributes; + +namespace Avro.Benchmark +{ + /// + /// Measures how long takes to resolve the generated classes of a schema + /// the first time, depending on how many types the application has loaded. A fresh + /// per operation keeps its type cache cold, which is what every + /// process pays once for every named schema type it deserializes. + /// + [SimpleJob(launchCount: 1, warmupCount: 2, iterationCount: 10)] + [MemoryDiagnoser] + public class ObjectCreatorBenchmarks + { + private const int NamedSchemaTypes = 50; + private const int TypesPerApplicationAssembly = 1_000; + + private string[] _namedSchemaTypeNames; + + /// + /// Number of types loaded in addition to the ones of the benchmark process, spread over + /// assemblies of 1,000 types each. An ASP.NET Core application loads roughly 25,000 types, + /// large applications several 100,000. + /// + [Params(0, 25_000, 100_000, 250_000)] + public int AdditionalLoadedTypes { get; set; } + + [GlobalSetup] + public void Setup() + { + for (int i = 0; i < AdditionalLoadedTypes / TypesPerApplicationAssembly; i++) + { + DefineAssembly($"Bench.Application{i}", TypesPerApplicationAssembly); + } + + // The generated classes live in their own assembly, like generated contracts referenced by a service + _namedSchemaTypeNames = DefineAssembly("Bench.Contracts.V1", NamedSchemaTypes) + .Select(type => type.FullName) + .ToArray(); + + Console.WriteLine($"// Loaded assemblies: {AppDomain.CurrentDomain.GetAssemblies().Length}, types: {CountLoadedTypes()}"); + } + + [Benchmark] + public void ResolveNamedSchemaTypes() + { + ObjectCreator objectCreator = new ObjectCreator(); + foreach (string name in _namedSchemaTypeNames) + { + objectCreator.GetType(name, Schema.Type.Record); + } + } + + private static IEnumerable DefineAssembly(string assemblyName, int typeCount) + { + AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( + new AssemblyName(assemblyName), + AssemblyBuilderAccess.Run + ); + ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName); + List types = new List(typeCount); + for (int i = 0; i < typeCount; i++) + { + types.Add( + moduleBuilder + .DefineType( + $"{assemblyName}.Type{i}", + TypeAttributes.Public | TypeAttributes.Class + ) + .CreateType() + ); + } + + return types; + } + + private static int CountLoadedTypes() + { + int count = 0; + foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + try + { + count += assembly.GetTypes().Length; + } + catch (ReflectionTypeLoadException e) + { + count += e.Types.Count(type => type != null); + } + } + + return count; + } + } +} diff --git a/lang/csharp/src/apache/main/Specific/ObjectCreator.cs b/lang/csharp/src/apache/main/Specific/ObjectCreator.cs index 073b107958a..60225f820a1 100644 --- a/lang/csharp/src/apache/main/Specific/ObjectCreator.cs +++ b/lang/csharp/src/apache/main/Specific/ObjectCreator.cs @@ -107,30 +107,10 @@ private Type FindType(string name) type = Type.GetType(name); } - // type is still not found, need to loop through all loaded assemblies + // type is still not found, need to look through all loaded assemblies if (type == null) { - foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - // Loading all types from all assemblies could fail for a variety of - // non -fatal reasons. If we fail to load types from an assembly, continue. - try - { - // Change the search to look for Types by both NAME and FULLNAME - foreach (Type t in assembly.GetTypes()) - { - if (name == t.Name || name == t.FullName || CodeGenUtil.Instance.UnMangle(name) == t.FullName) - { - type = t; - break; - } - } - } - catch - { - continue; - } - } + type = FindTypeInLoadedAssemblies(name); } return type @@ -288,6 +268,79 @@ public Type GetType(Schema schema) return FindType(schema.Name); } + /// + /// Finds the type with the given name in all loaded assemblies. If several assemblies contain + /// a matching type, the one found last in the loaded assemblies wins. + /// + /// the object type to locate + /// the object type, or null if not found + private static Type FindTypeInLoadedAssemblies(string name) + { + Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); + string unmangledName = CodeGenUtil.Instance.UnMangle(name); + + // A name with a namespace can only match the full name of a type. Looking it up directly + // in each assembly finds the same type as enumerating all types of all assemblies, without + // loading all of them. + if (name.IndexOf('.') >= 0) + { + Type type = null; + foreach (Assembly assembly in assemblies) + { + try + { + type = assembly.GetType(name, false) ?? assembly.GetType(unmangledName, false) ?? type; + } + catch + { + // Looking up a type could fail for a variety of non-fatal reasons, continue. + } + } + + if (type != null) + { + return type; + } + } + + return FindTypeByEnumeratingLoadedAssemblies(assemblies, name, unmangledName); + } + + /// + /// Finds the type with the given name or full name by enumerating all types of all loaded assemblies. + /// + /// the loaded assemblies + /// the object type to locate + /// the object type to locate, without escaped reserved words + /// the object type, or null if not found + private static Type FindTypeByEnumeratingLoadedAssemblies(Assembly[] assemblies, string name, string unmangledName) + { + Type type = null; + foreach (Assembly assembly in assemblies) + { + // Loading all types from all assemblies could fail for a variety of + // non -fatal reasons. If we fail to load types from an assembly, continue. + try + { + // Change the search to look for Types by both NAME and FULLNAME + foreach (Type t in assembly.GetTypes()) + { + if (name == t.Name || name == t.FullName || unmangledName == t.FullName) + { + type = t; + break; + } + } + } + catch + { + continue; + } + } + + return type; + } + /// /// Gets the type of the specified type name /// diff --git a/lang/csharp/src/apache/test/Specific/ObjectCreatorTests.cs b/lang/csharp/src/apache/test/Specific/ObjectCreatorTests.cs index d6b29d5d954..33099acc09a 100644 --- a/lang/csharp/src/apache/test/Specific/ObjectCreatorTests.cs +++ b/lang/csharp/src/apache/test/Specific/ObjectCreatorTests.cs @@ -20,6 +20,9 @@ using NUnit.Framework; using System; using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; namespace Avro.Test.Specific { @@ -103,6 +106,48 @@ public void TestComplexGetTypes(Type expecteType, string name) Assert.AreEqual(expecteType, objectCreator.GetType(name, Schema.Type.Record)); } + [Test] + public void TestGetTypeByFullNameFromLoadedAssembly() + { + var name = $"Avro.Test.Dynamic{Guid.NewGuid():N}.Contracts.MyRecord"; + var expected = DefineAssemblyWithType(name).GetType(name); + var objectCreator = new ObjectCreator(); + + Assert.AreEqual(expected, objectCreator.GetType(name, Schema.Type.Record)); + } + + [TestCase("Avro.Test.Specific.return.Record", TestName = "TestGetTypeReservedWordInNamespace_Unmangled")] + [TestCase("Avro.Test.Specific.@return.Record", TestName = "TestGetTypeReservedWordInNamespace_Mangled")] + public void TestGetTypeReservedWordInNamespace(string name) + { + var objectCreator = new ObjectCreator(); + + Assert.AreEqual(typeof(Avro.Test.Specific.@return.Record), objectCreator.GetType(name, Schema.Type.Record)); + } + + [Test] + public void TestGetTypeFullNameInSeveralAssembliesResolvesToLastLoadedAssembly() + { + var name = $"Avro.Test.Dynamic{Guid.NewGuid():N}.Contracts.MyRecord"; + var first = DefineAssemblyWithType(name); + var second = DefineAssemblyWithType(name); + var lastInLoadOrder = AppDomain.CurrentDomain.GetAssemblies() + .Last(assembly => assembly.FullName == first.FullName || assembly.FullName == second.FullName); + var objectCreator = new ObjectCreator(); + + Assert.AreEqual(lastInLoadOrder.GetType(name), objectCreator.GetType(name, Schema.Type.Record)); + Assert.AreNotEqual(first.GetType(name), second.GetType(name)); + } + + private static Assembly DefineAssemblyWithType(string typeFullName) + { + var assemblyName = $"Avro.Test.Dynamic{Guid.NewGuid():N}"; + var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(assemblyName), AssemblyBuilderAccess.Run); + var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName); + moduleBuilder.DefineType(typeFullName, TypeAttributes.Public | TypeAttributes.Class).CreateType(); + return assemblyBuilder; + } + private class MyNullableFoo { }