0 The road to Ahead of Time optimization (AoT)
David Shiflet редактировал(а) эту страницу 2023-04-07 12:25:44 -05:00

Overview

AoT is shorthand for a .Net technology to produce native binaries that can run with no JIT compilation. We have a long-term goal to make SqlManagementObjects core pieces buildable as AoT DLLs.

Benefits of an AoT build

  • Substantial performance improvements.
  • We can export core scenarios to be callable from non-.Net applications using [UnmanagedCallersOnly] attribute.

External obstacles

The main external dependency that is a long way from AoT readiness is Microsoft.Data.SqlClient. As we make progress on AoT in SMO we will open issues against that component. Hopefully in their version 6 they can break up their monolithic driver into smaller pieces (such as removing the MSAL dependency) and the core driver could be built as AoT.

Internal obstacles

Dependence on reflection

Much of that dependence is based on SFC.  For example, a URN like Server/Database[@Name='mydb']/Option is mapped at runtime to a property on the database object by examining attributes on Type instances.

In this example, we see how Option is mapped to the DatabaseOptions property of the Database type.

The property declaration of Database.DatabaseOptions has an attribute indicating a ChildObject relationship:

        [SfcObject(SfcObjectRelationship.ChildObject, SfcObjectCardinality.One)]
        public DatabaseOptions DatabaseOptions

The DatabaseOptions type is attributed to map its URN key:

    [SfcElementType("Option")]
    public partial class DatabaseOptions : SqlSmoObject, Cmn.IAlterable

These attributes come into play when SMO is trying to find or create an object from the URN, the core of which is in this function in SqlSmoObject

        private void InitObjectsFromEnumResultsRec(SqlSmoObject currentSmoObject,
            XPathExpression levelFilter,
            int filterIdx,
            System.Data.IDataReader reader,
            int columnIdx,
            object[] parentRow,
            bool forScripting,
            List<string> urnList,
            int startLeafIdx)

That function has a bunch of special cases, it mixes hard coded switch statements with reflection, and it uses exceptions-as-flow-control. Let's follow this code.

            Type childType;
            if (urnList != null)
            {
                // We have to special case Server here, since we normally skip over it for Object Query
                // except when it is all by itself (i.e. the query is for just "Server").
                int nodeCount = levelFilter.Length;
                childType = GetChildType((
                    nodeCount > filterIdx) ? levelFilter[filterIdx].Name : currentSmoObject.GetType().Name,
                    currentSmoObject.GetType().Name);
            }
            else
            {
                childType = GetChildType(levelFilter[filterIdx].Name, currentSmoObject.GetType().Name);
            }

Here it's looking at the current part of the URN identified by filterIdx to see what property in currentSmoObject is being fetched. GetChildType relies completely on the Name attribute of the URN node and the Name of the Type of the current object. The comments above GetChildType are informative:

        // TODO: FIX_IN_KATMAI: This function is messed up beyond repair. It needs to be completely rewritten
        // using SfcMetadata

        // this function figures out the child type looking at the urn name and the parent name
        // we need this function because names from urn do not fit the object names all the time
        public static Type GetChildType(string objectName, string parentName)

That function has a huge switch statement because there's no reflection-based way to map a member that's a collection to its URN name. For example, both Database and Server have a Roles member, but they are collections of different types:

Type GetChildType {
...
                case "Role":
                    if (parentName == "Server")
                    {
                        realTypeName = "ServerRole";
                    }
                    else
                    {
                        realTypeName = "DatabaseRole";
                    }

                    break;
...
DatabaseBase.cs:

        [SfcObject(SfcContainerRelationship.ObjectContainer, SfcContainerCardinality.ZeroToAny, typeof(DatabaseRole), SfcObjectFlags.Design)]
        public DatabaseRoleCollection Roles

ServerBase.cs:
        [SfcObject(SfcContainerRelationship.ObjectContainer, SfcContainerCardinality.ZeroToAny, typeof(ServerRole))]
        public ServerRoleCollection Roles

Notice the SfcObject attribute doesn't have any parameter to provide the URN component name, and there's no SfcElement attribute on the declaration that could be used instead.

Going back to InitObjectsFromEnumResultsRec, we then find a slew of special cases to initialize child objects of specific types.

            if (childType.Equals(typeof(DefaultConstraint)))
            {
...
                    else if (childType.Equals(typeof(TcpProtocol)))
                    {
                        obj = ep.Protocol.Tcp;
                    }
                    if (null != obj)
                    {
                        InitObjectsFromEnumResultsRec(obj, levelFilter,
                            // next filtering level
                            filterIdx + 1,
                            reader,
                            // no key, so we don't advance the column index
                            columnIdx,
                            parentRow,
                            forScripting,
                            urnList,
                            startLeafIdx);
                    }
                }
                return;
            }

Those special cases are hard to maintain and lead new SMO contributors to implement more special cases when perhaps they didn't need to. It's clearly a code design problem to have the base class SqlSmoObject breaking encapsulation to know about all its derived types.

At this point the function has either identified the type of object identified by the URN segment or it has actually populated child object and returned so the next segment can be process recursively. In the former case we are now ready to populate the child object using a more generalized mechanism. Here it assumes that the child object is a collection until proven otherwise:

            AbstractCollectionBase childColl = null;
            bool isNonCollection = false;

            try
            {
                childColl = GetChildCollection(currentSmoObject, levelFilter,
                    filterIdx, GetServerObject().ServerVersion);
            }
            catch (Exception e)
            {
                // The old InitChildLevel caller just wants to throw here. It never handled singletons here anyhow.
                if (urnList == null)
                {
                    throw;
                }

                if (!(e is ArgumentException || e is InvalidCastException))
                {
                    throw;
                }

                // We come here if we ask for a child collection for a level that isn't really a collection like singletons.
                // Since we still want to get into AdvanceInitRec with them, we need to process them differently.
                // Only do this for the Object Query case (urnList != null), not the old InitChildLevel cases.

                isNonCollection = true;
            }

GetChildCollection leads us to dynamic code execution.

        internal static AbstractCollectionBase GetChildCollection(SqlSmoObject parent,
            string childUrnSuffix, string categorystr, ServerVersion srvVer)
        {
            // this actually supposes that all child collection are named like this
            // For some classes we have to get the gramatically correct plural
            string childCollectionName = GetPluralName(childUrnSuffix, parent);
            object childCollection = null;

            // Permissions is an internal property, but we do not want to open the
            // door to calls to internals methods so we limit the use of
            // BindingFlags.NonPublic to Permissions
            if (childCollectionName != "Permissions")
            {
                try
                {
                    childCollection = parent.GetType().InvokeMember(childCollectionName,
                                BindingFlags.Default | BindingFlags.GetProperty |
                                BindingFlags.Instance | BindingFlags.Public,
                                null, parent, new object[] { }, SmoApplication.DefaultCulture);
                }
                catch (MissingMethodException)
                {
                    throw new ArgumentException(ExceptionTemplates.InvalidPathChildCollectionNotFound(childUrnSuffix, parent.GetType().Name));
                }
            }
            else
            {
                childCollection = parent.GetType().InvokeMember(childCollectionName,
                        BindingFlags.Default | BindingFlags.GetProperty |
                        BindingFlags.Instance | BindingFlags.NonPublic,
                        null, parent, new object[] { }, SmoApplication.DefaultCulture);
            }

            // this should always be true in SMO, a collection will never return null
            // but it can be empty
            Diagnostics.TraceHelper.Assert(null != childCollection, "null == childCollection");

            return (AbstractCollectionBase)childCollection;
        }

This code is why every SMO collection has an internal constructor that accepts a SqlSmoObject parameter. Of course, if that InvokeMember call throws, what happens? InitObjectsFromEnumResultsRec decides that the member isn't a collection, it's a singleton and sets isNonCollection = true.

            if (isNonCollection)
            {
                SqlSmoObject currObj = GetChildSingleton(currentSmoObject, levelFilter,
                    filterIdx, GetServerObject().ServerVersion);

                if (!AdvanceInitRec(currObj, levelFilter, filterIdx, reader, columnIdx,
                    columnOffset, parentRow, forScripting, urnList, startLeafIdx))
                {
                    return;
                }
            }
...
SqlSmoObject GetChildSingleton(SqlSmoObject parent,
            XPathExpression levelFilter, int filterIdx, ServerVersion srvVer) {
...
            // Do we already know what parent property points to the singleton instance?
            if (!s_SingletonTypeToProperty.TryGetValue(childType, out propName))
            {
                SfcMetadataDiscovery metadata = new SfcMetadataDiscovery(parent.GetType());
                foreach (SfcMetadataRelation relation in metadata.Relations)
                {
                    if (relation.Relationship == SfcRelationship.ChildObject ||
                        relation.Relationship == SfcRelationship.Object)
                    {
                        if (childType == relation.Type)
                        {
                            // Found it
                            propName = relation.PropertyName;
                            break;
                        }
                    }
                }

                // Cache this result for the next time we need to lookup this type.
                // Propname will be null if we know it isn't possible to use this type.
                lock (((ICollection)s_SingletonTypeToProperty).SyncRoot)
                {
                    s_SingletonTypeToProperty[childType] = propName;
                }
            }

            if (propName == null)
            {
                throw new ArgumentException(ExceptionTemplates.InvalidPathChildSingletonNotFound(childType.Name, parent.GetType().Name));
            }

            try
            {
                childObject = parent.GetType().InvokeMember(propName,
                            BindingFlags.Default | BindingFlags.GetProperty |
                            BindingFlags.Instance | BindingFlags.Public,
                            null, parent, new object[] { }, SmoApplication.DefaultCulture);
            }
            catch (MissingMethodException)
            {
                throw new ArgumentException(ExceptionTemplates.InvalidPathChildSingletonNotFound(childType.Name, parent.GetType().Name));
            }

            return (SqlSmoObject)childObject;
}

Notice that GetChildSingleton knows what property the URN component name maps to because of the SfcElement attribute on the property declaration.

Circular dependencies

There are many instances of a low level component like Sfc or Smo having a list of types in other assemblies that it invokes dynamically. One common usage is to support Policy Based Management in SQL Server. SMO is hosted in SqlClr to implement policy evaluation and facets are identified with various Sfc attributes. The domain declarations are centralized in SfcRegistration.cs

Action items

The road to AoT is a long one and it's hard to foresee all the work that needs to be done. We can identify a few ways to make progress now.

Implement SMO collections using generics

Define interfaces and attributes to replace special cases

  • Add SfcElement attributes to collection members to eliminate special case lookups
  • Define interfaces to encapsulate the special cases for child object initialization and implement them on the object types that need them

Replace the runtime use of Sfc relationship attributes with a C# code generator

See https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview for information about these code generators. Once we have properly attributed the members to map them to URN components and implemented interfaces to encapsulate special cases for initialization, we can move the logic that maps the [parent type, urn component] tuples to an initialization method into a static class that is created by the source generator.

Remove circular dependencies

These dependencies will need further examination to derive a better design that preserves backward compatibility.