I have been thinking a bit about how to tackle the lack of support for external modules within the Salesforce packaging toolset. By external modules I really mean external to the source tracking repo that you are working on. I have partially implemented an approach in some of my own tooling but thought it would be worth posting about so others can chip in and improve.
External module dependencies are declared in an extension to the Salesforce sfdx-project.json schema. As an extension it uses the “plugins” property which is open for use by third-parties to add additional configuration data to sfdx-project.json.
An external dependency is metadata that is available for use but not described by the “packageDirectories” entries of sfdx-project.json. The dependency metadata can only be provided as source although that may be in either MDAPI or SFDX format (aka source format). The location of the metadata may be described in multiple ways, via filesystem path, via url or via npm module name.
The purpose of declaring these project dependencies is to allow developer & CI tooling to have full visibility of the metadata that may need to be deployed to an org, but crucially to maintain the flexibility to compose that metadata from a number of separately managed modules.
There is some overlap in objectives here with Salesforce’s own second generation packaging model. The distinction is that external module dependencies do not imply any particular form of packaging is in use. You can choose to package an external modules dependency on its own or to bundle it with other modules and application code into a package in either first or second generation form.
In sfdx-project.json we include a “dependencies” array within the “plugins” property to indicate modules that can be imported.
All dependencies must identify a source for the metadata, either as a “path” or “npm” property, and a target path. The source and correct version for a “npm” metadata is assumed to have been pre-loaded in the node_modules directory. It is provided as an alternative to “path” to aid version conflict resolution.
An optional namespace may also be provided which may differ from the declared namespace in the sfdx-project.json file of the dependency.
Dependent Module Installation
Dependencies are installed into the metadata via a namespace transformation and file copying into the “target” location. The target must be an existing “packageDirectories/path” directory or descendant path into which the files can be copied without overwriting existing metadata.
As modules may themselves declare dependencies this process must be recursively executed. Where multiple different versions of a “npm” module are identified only the version with the highest semantic version should be installed.
Namespace transformation is applied where the “namespace” property is provided and different from the namespace property in the modules sfdx-project.json file if it has one. The transform changes all occurrences of ‘<namespace>__’ & ‘<namespace>.’ from the declared namespace to the target namespace.
Module installation may create name clashes within the metadata, such as two Apex classes of the same name in different folders under the same “packageDirectories/path” directory. While these may be detected during an install they can also be ignored as package creation will identify them.
Dependent Module Cleaning
Dependent modules are cleaned from the “packageDirectories/path” directories by following the same approach as ‘Installing’ but replacing the file copying operations by deletions of the target file.
These are the first podcasts I have done and I was fairly nervous about my ability to handle this and communicate the more complex details of ApexLink clearly. In the end I think these turned out really well thanks to Xi’s guiding the conversation and his skills in editing.
For those not familiar with ApexLink it’s a Library I have been working on for a few years to perform Apex static analysis. You can use it in a few different ways, from a VSCode extension in a sfdx cli and hopefully in the near future as an add-on for PMD.
An area of Apex runtime that I have often found difficult to grasp is how the Schema namespace works. As part of work on ApexLink I had to explore this so thought I best write some notes about what I found and then thought maybe other Salesforce developers would be interested in some of this so lets make it a blog…
A namespace in Apex is similar to a package in Java, it’s a container within which you will typically find the familiar classes, interfaces and enums which I will collectively refer to as types. There are quite a lot of namespaces in the Apex runtime, ApexLink has definitions for 37 but only a few are commonly used.
Two of the namespaces (System & Schema) are significant in that you do not have to use the namespace name to use classes from these. When the Apex compiler is searching for a type it will automatically search these namespaces. You can qualify types in these namespaces with the namespace name if you want, that can sometimes be useful if a type name is ambiguous, but generally it’s not needed.
If you look at the Schema namespace documentation you will find the types shown in the figure. These are the types that are always available but on any given Org you will find lots of other types here that are created to ease access to database records and other types of metadata.
The simplest of these are the SObject types, such as ‘Schema.Account’, which in most Apex code is just written as ‘Account’ since we like typing less characters. Other commonly used types of metadata you can find in Schema include custom settings, custom metadata & platform events.
These additional types are useful to allow you to refer to types statically by name. So in Apex I can simply write:
To create a new Account record that I might later insert to the database.
Org’s, Packages & Lazy Loading
In ApexLink I choose to use a lazy loading strategy for the Schema namespace. There were a couple of reasons for this but you can probably skip this section if you are just interested in learning more about the Schema namespace, come back if you get lost later on.
The ApexLink API provides an API that models a simulated Org. By this I mean to use the API you create and ‘Org’ and inside that you create ‘Packages’ identifying where to load the package metadata from and what other packages they depend on. The Org & Package here are just objects in memory but they give me a way of thinking about metadata management which is similar to how actual Orgs and Packages work which I found useful.
A key change I made though was to enforce that Apex code in a package can only reference types within its own package or those exposed from packages it depends on or the platform provided types. This is a stricter model then is enforced on actual Orgs but it is useful for package developers because it can help us detect if we are using things we should not. This model however has its complications in that for each Package we need to isolate the types that can appear in namespaces like Schema from what another package may be able to use.
In ApexLink this is achieved by lazy loading the additional types needed into the Package that needs them at the point of first reference. This helps reduce memory and cpu usage during the ApexLink analysis. So as the Apex code that creates an Account is analysed the definition of an Account is loaded into the Schema namespace for that Package, if it has not already been loaded before.
In addition to providing types to assist in writing Apex the other feature the Schema namespace provides is to describe the shape of metadata. In Java we would call this feature reflection but Apex only provides describe support for a pretty limited set of types and the supported capabilities are much reduced. The core of this feature is Schema.SObjectType which can be obtained in a few ways.
The third case here is interesting as we appear to be accessing SObjectType twice. ‘SObjectType.Account’ is returning a ‘DescribeSObjectResult’ from which we can then get the actual SObjectType. I added the forth case just for fun, you can do this, I have no idea why it works.
Before going further I should also mentions that the performance of these may vary significantly on real Orgs. Traditionally accessing ‘describe’ data has been expensive since it needs to be pulled into a cache on the server that runs the Apex code. I have not benchmarked these but I would expect that the third one will not be cheap the first time any code uses ‘describe’ data for Account. If you want to know more about describe performance you must read this blog by Chris Peterson.
While looking at SObjectType I noticed an oddity in how it behaves that is shown by this code.
I think most Apex programmers know there is some weirdness in this area but often don’t grasp it because there is no detailed documentation on how Apex works. In this case though you can smell there is something a miss by observing the Account.SObjectType clearly can’t return the same type as say Contact.SObjectType because they have different fields.
What I thought was likely happening here is that ‘Account.SObjectType’ does not return an SObjectType but something derived from a SObjectType, let’s think of it as an ‘AccountSObjectType’ and when I assign that to the generic SObjectType access to the ‘Account’ specific parts are no longer accessible.
In ApexLink, rather than create an SObjectType for each I re-used the generics support needs for List, Set, Map etc and instead of Account.SObjectType used SObjectType<Account>. To cover my magic I then hide this generic type whenever I need to print it by displaying it as ‘Account.SObjectType’, i.e. the thing that generates it rather than what it is.
Hiding my tracks here feels pretty dirty but is necessary to avoid introducing a new abstraction that is visible to programmers. Why Salesforce hid this will become a bit clear later on but as we are about to see the kinds of decisions tend to cascade on you in runtime designs.
In the last section we saw accessing fields directly on an SObjectType to get an SObjectField but you can also access them via the ‘fields’ field. Before looking at that I want to point to a difference here as there is another ‘fields’ field:
I am interested in the second one here, available on SObjectType, I will come back to DescribeFieldResult later on. I have yet to find a description of what ‘fields’ is in this case, if you try to evaluate it without adding a field name a null is returned which is not a great help.
From an ApexLink perspective I again turned to generics to handle this, so ‘fields’ is typed as a SObjectFields<Account> type. For those wondering, although I use generics carry the SObject type here there is another trick being used to allow the fields accessible on a Type to vary independently of that type. At the core of the analysis in ApexLink you don’t iterate over a fixed set of available fields available on a type but call a function ‘findField(name)’ on the type instance. This function is free to return fields from some pre-existing set or may construct fields dynamically as needed which allows the visible fields on SObjectFields<Account> to be different to those on SObjectFields<Opportunity>. In this case I use the type argument, like ‘Account’ to work out what fields should be findable.
It’s still a bit of mystery to me why we can both access the fields directly on SObjectType instances and via ‘fields’, you would have thought one would have been fine. Currently I use the same code to implement findField() for either case but maybe someone can point to a difference between them.
Schema.SObjectType Statics & Describes
Let’s go back a bit and look at the Schema.SObjectType static fields. If you are anything like me at this point your head is starting to spin a bit, so let’s quickly recap. The previous discussion has been focusing on instances of SObjectType but that just part of what is hidden here. In this section we are focusing on the static fields that you can also find on SObjectType.
On SObjectType you can access the ‘DescribeSObjectResult’ and from that the ‘DescribeFieldResult’. We can again demonstrate the problem if you split these.
In this case though there is some documentation that debunks my theory that this was being caused by ‘SObjectType.Account’ not returning a DescribeSObjectResult but something derived from it.
It’s clear from this that the Apex parser has been made able to understand the objects and fields available at runtime but only in very specific contexts. This is something of an anti-pattern in language design exactly because of the confusion it creates between the syntactic and semantic domains that programmers use to help understand errors. In short I don’t understand why the example breaks because my programmer brain is wired to see this as not possible if syntax is cleanly separated from semantics.
From an ApexLink perspective though this is very similar to what we have seen before and can be handled by treating ‘SObjectType.Account’ as returning a hidden generic type that inherits from DescribeSObjectResult so it is assignable to it in a way that will mimic this behaviour. There is almost matching behaviour for FieldSet access which is dealt with in the same way.
Using generics and findField() handling as got me past most problems in the Schema namespace in ApexLink but there were a number of other challenges so I will mention them briefly here in summary.
Alongside the main type for some metadata we also have to create the companion objects such as Share, History & Feed types. Mostly these are straight forward but RowCause in the Share object required some special handling for its SObjectField.
When dealing with Lookup and Master/Detail fields the relationship fields need to be created. This has forced me to load all SObject related metadata on startup so the correct related lists can be created. I still have some difficulty here with Rollups which can also create a dependency order during metadata loading which I need to resolve.
Separating out the various types of metadata that can cause things to appears in the Schema namespace has been rather difficult, not least because in SFDX you can omit the ‘object-meta’ files when adding fields to an existing type which makes detection of which directories contain metadata more complex. [Ed: You should think about extracting your metadata identification and parsing code into a separate library as other might find that useful.]
There is complexity in ApexLink around determining if to create a new type of extend and existing one when say we are adding a custom field. This is fairly inherent in the problem domain but the relationship between ‘Activity’ with ‘Task’ & ‘Event’ needed some special case handling.
The idea of there being standard fields on each metadata types has cause quite a lot of grief. In many cases it turns out these are not as standard as you might think they should be, they are often mere conventions (with exceptions).
In the few years I have been working with Salesforce one of things that has bugged me a lot is how long unit test suites take to run. I am going to explain here how we have finally made progress on this, rather excellent progress as Bill & Ted might have told it.
The short version is that we have automated what devs have been doing for a while by building a custom SFDX CLI command, this first runs the tests in parallel and then mops up any of those tests that fail with the infamous UNABLE_TO_LOCK_ROW by running them again. The automation makes things easier, but also gives us the chance to add some extra resilience to the process.
Prompted by a question on Salesforce StackExchange I have made some code that follows this approach available at github. It’s a bit embedded in something else but you can either chase through to see how this works or carry on reading and I will explain. Mostly what it is doing is orchestrating the running of standard sfdx commands to get the right output. If you just want to play with a pre-package version see instructions at bottom.
As tests runs can still take a little time the sfdx cli command reports progress so let’s start with that.
This shows a test run of a suite that if ran sequentially would take 8-9 hours to complete. I have changed the methods names just to protect the innocent but everything else is accurate, including that I had a few hours free late on a Saturday that a choose to use working on this ;-(
There are two stages being executed here, the first just runs all local tests on the org in parallel. While this runs the command reports progress roughly every minute so you can see what is happening. After that the tests failing due to locking issues are identified and these are re-run sequentially, if they pass during this phase then they are removed from the failure list. At the end of the run a JSON file is generated with any tests that have not passed.
It has taken a bit of effort to make this CLI stable but the approach taken is being used on a fairly busy CI pipe with very few issues. If you have spent much time working with SFDX CLI you will know its still a bit on the immature side and some of the effort in writing this has been in trying to anticipate when things might fail and deal with them.
The first issue we had to deal with is that force:apex:test:report would not handle this many tests, it has its limit. To workaround that we do use force:apex:test:run to start a test run & then we run force:apex:test:report but only as a means to be notified when the test run has completed. Unfortunately this was not that stable so there is some handling in the code to restart waiting if it dies unexpectedly alongside dealing with it failing due to too many tests.
Without being able to use force:apex:test:report the command implements its own reporting via running SOQL queries on the results. We also use SOQL queries for reporting progress every minute, this is just for progress so the data returned is not used for anything else.
Tests that fail due to locking are identified by looking for a couple of patterns in the result message. Once these are identified they are run again via force:apex:test:run but with the –synchronous flag set and naming the methods we want to re-test.
In the implementation I linked to above the final act is to save the list of failure details in a file as JSON. We have found it useful here to duplicate how force:apex:test:report prepares output to make CI integration easier. I have not included that handling in this code as it’s a bit involved, maybe I will add it later. If you want to see how this is done in force:apex:test:report have a look at testResults.js in the salesforce-alm package of the cli installation.
We have looked at a couple of ways you might make this run quicker. Initially we were quite excited by the possibilities of annotating test classes with @isTest(isParallel=true). Sadly after rather a lot of time spent on this we concluded it has no significant impact on our tests and it’s a bit of pain to do since not all tests can use it, maybe yours will be different.
What does help though is removing batch tests. If you look at the progress information you might note the number of tests executed in a period is reducing during the run. We spotted this and tried removing the batch tests and found the execution time on one suite tested halved. I don’t really have any insight into why Batch jobs hold everything up but something to be aware of.
Install via sfdx cli
The code for this is available as a sfdx cli extension on npm. To install run:
sfdx plugins:unistall apexlink
This might take a little while as the package has some Java code in it that I use for parsing Apex. To run the command make sure you are in a sfdx project and authenticated to an org and then do:
There are some arguments on the command but none are functional just yet.
You have almost certainly seen the impact of class caching at some point, or rather cache misses, when some page takes an unusually long time to load the first time you try and use it. At work we generally talk of these events as being ‘cold starts’ which is a play on a cold cache. However, they can also be caused by invalid class recompilation so to avoid any confusion in this post I am only going to look at cold starts caused by a cold caches. None of the tests I did in this post involve invalid class recompilation.
What do they look like?
It turns out cold starts are actually quite hard to capture. I had to run quite a lot of tests to get a few samples, 9 days worths of tests actually:
This is showing the execution time for an anonymous apex call measured every hour for those 9 days. The spikes here are what we are looking for, calls that takes significantly longer than the average. There is no real pattern as to when these occurred, some were in the middle of the night, other during work hours. Some clustered close together, others in isolation.
I am using an anonymous Apex just because it was easy to run from my test harness. Customers normally see this type of behaviour when trying to access pages but the real cause is somewhere in the class cache system as this org was not being used by anyone while this test was running so no classes could have been invalidated. A number of these cold starts may be being caused by maintenance work on the instance, there is really no way to tell the root cause but the impact is obvious.
If you have watched the new compiler talk you might recall that there are two caches used for Apex classes, a level 1 cache on each application server and a level 2 cache shared between application servers. It’s not clear from this data which cache is causing these spikes. I would like to understand that but pragmatically to our customers it does not really matter, if they see this kind of behaviour often enough they are going to start questioning our ability to write good software.
The test result above are from a binary tree of classes with a call to a method on the root node being timed. This method calls the same method on its child class nodes which in turn call the same method on their child classes. This of course requires all the classes in the tree to be loaded for the call to complete. For this test I used a tree with 2048 classes and added 1kB of unrelated Apex code to each.
As I knew this was going to take some time to run I ran the same tests on four other trees of different sizes at the same time so we can compare the impact. Each of these trees has the same total amount of code spread over the classes just so any costs due to code size could be ignored. Looking at one day we get this:
Here we have a couple of cold start issues early in the morning. This looked very much like the primary cost was related to the number of classes so I used the data to calculate a cost/class when a cold start happens to get this:
What I think this is telling us is that the cache miss cost is mostly a factor of the number of classes but bigger classes do have some impact. There is some part which is proportional to the amount of code you put in the class which is consistent with the description of ‘inflation’ from the new compiler talk.
Should I worry about this?
This is a hard question to answer. The pages & triggers of your products need to require quite a lot of classes for the cold start to be significant so this is not really a problem on smaller products but it also depends on how tolerant your customer are to response times.
What you should be wary of if you already have or suspect you will have response time concerns is how you architect and design your product. Using lots of small classes will help your deploy times but could also increase the impact of cold starts. What I can’t really shed much light on is if the same sort of patterns are common across instances or if the incidence varies much over the medium/long run of months, this is just a snapshot of one week on one instance.
My takeaway from the results in this post and the last two is that there are things I am doing when coding that I have not thought through the impact they have on my own developer experience and customer response times. This feels poor, but as yet I don’t know how to correct this by giving myself a new mental model of what I should be aiming for. If I find a happy place I will let you know…
In my last post some testing showed how Apex class deploy times are proportional to the size of code in the class being deployed and all its dependant classes. If you read this alone you might think that best practice would be to always write very small classes. What we also have to consider though is what impact that will have on our customers experience. In this post I am going to focus on one aspect of this, what happens to response times when there are invalid classes in an org.
Lets start with the headline result:
This graph is showing how long it takes to recompile classes of various sizes once they have become invalid. By re-compile here I really mean the process that converts an invalid class to a valid one on-demand as some execution context attempts to use it. I think it’s fair to assume this a recompile of the class source code into the byte code format used by Salesforce given we have a ‘Compile All Classes’ link that appears to do exactly this operation.
As you can see there is fairly high initial cost of ~20ms/class + a variable component depending on class size. The time here is measured as the additional time needed to make an anonymous Apex request when that request uses classes which are invalid. So if the request required ten 8 kB sized classes to be recompiled we would expect the request to take 0.5seconds longer than if those classes were not invalid. You could also likely time this on the platform as well as calling Type.forName on a class with causes it to become valid but I am using the Tooling API for tests so timing some Anonymous Apex that will invoke a method on the class is more convenient.
To calculate this I used the same binary tree of classes approach from this post but made a few adjustments. When the tree is initially constructed all the classes are in a valid state, to invalidate part of the tree the test updates all the leaf node classes which causes all non-leaf node classes to become invalid. The test then times an anonymous Apex call to the root node class which will in turn call methods on all classes in the tree requiring any invalid classes to be recompiled. To get the additional cost of the re-compile I ran the test with and without the code that invalidates part of the tree and subtracted the results before dividing by the known number of invalid classes in the tree. The number reported is geometric mean of 10 runs.
This was the second test I ran, the earlier test showed that the cost to recompile invalid classes appears to be linear to the number of invalid classes. I tested that up to ~512 invalid classes so there is a possibility that the linear relationship will break down a bit after this. I wasn’t really expecting this to show anything interesting but you never know.
Ideal size & compile on deploy
This result does leave us with a bit of a problem. We would really like to have class size driven by ‘clean code’ considerations, not the implementation details of Salesforce orgs, but that may well hurt either our productivity via excessive class deploy times or our user experience via poor responsiveness depending on what turns out to be a typical class size for the type of product we are developing.
Salesforce have recently introduced ‘Compile on deploy’ support to try and address the responsiveness side of this problem. My experience with this so far has been pretty disastrous, if enabled developer deploys for single classes can go from 30 seconds worst case to many minutes. Worst still, on some org types you don’t have the ability to disable via Apex Settings, it’s locked on.
The positive side of compile-on-deploy is it should mean that on customer orgs the cost of invalidation recompiles is not important anymore and so using the smallest possible classes looks desirable to improve developer experience. There is however another fly here, the class caching that is used to improve the runtime access to recently used classes. Studying caching behaviour is always difficult but maybe I can find a way to look at what is happening to get a more complete picture.
Inner classes to the rescue?
Thinking about how these result might change my coding style it feels obvious that my own best interest is served by making classes as small as possible in an attempt to reduce class update times to the minimum possible to improve my own productivity. Doing that though may well cause issues with system response times depending on the compile-on-deploy setting and class caching behaviours.
Perhaps a way around this is write outer classes as though they were inner classes (i.e. no inner classes or statics). That would allow for a very easy route to automatically convert a set of outer classes into inner classes which can be combined together to reduce the class count needed. In a DX project where we can at last use directories to organise code you could place all the related classes together and auto-merge them for packaging. Just a thought…
When working with multiple managed packaged I have often wondered why single file deploy times vary so much. Classes in some packages only take a few seconds to deploy while in other packages classes with similar purpose can consistently take much longer. The difference can be very large, it often feels like up to 10 times worse.
I have performed some experiments to try and understand what is happening but before we get there I should mention this is more than casual interest for me. If you do a bit of research you will find the link between poor system response times and developer productivity has been studied a few times. While the evidence is not entirely conclusive (for me) it’s pretty strong. How much time developers lose due to long deploy times is always going to be hard to gauge, but if I ask my colleagues at work about this link no one argues it does not exist.
Show me the data!
Ok, fair enough. Let’s start with this:
This is a log of the time taken to deploy individual classes taken from the same managed package via the tooling API. Each dot represents one class with the y-axis showing an average milli-seconds taken to deploy over 10 attempts. The data is presented in the order it was collected along the x-axis.
The first thing you might notice is that around about the 100th class tested there was some kind of brownout. The deploys continued to work but were much slower for a few minutes. I didn’t investigate the cause of that but it’s not what interested me, I was really interested in the time distribution of the other deploys.
There is a clear indication here of two clusters, classes that deploy in ~3.5 seconds and those that typically take 7 seconds. Repeating this test on a different package shows that 3.5seconds is virtually always best case but the higher cluster location location varies, so in the other package I tested the higher cluster was closer to 20 seconds. Another observation is that percentage of classes in each cluster can vary. In the graph above the two clusters are roughly equal in size but in another case approx ~90% of classes tested were in the slower to deploy group.
This is clearly quite unusual behaviour, I was expecting to find some kind of distribution to deploy times but not quite this. Understanding why we see these clusters requires delving into code patterns which I will likely do in a future post but for now we can learn more by studying the deploy time behaviour of some generated classes.
My first thought on seeing the graph above was that the deploy times differences were being caused by the need to invalidate other classes. If you have spent much time developing with Apex you will know classes can becomes ‘invalid’. To see this go to the Setup->Apex Classes and add the IsValid flag to the view. The flag is used to indicate that although the class was valid when deployed (all are) some metadata has changed that might mean it is no longer syntactically valid. The flag is indicating to the runtime that it will need recompiling/rechecking before next use. You can get rid of all the invalid classes by hitting the ‘Compile all classes’ action on that page.
To test this I generated long chains of classes that called a method on the next class in the chain so they became dependant. If you then update the class at the end of the chain you can then force a large number of classes to become invalid during the update. This test did show part of the deploy time was coming from the need to perform invalidation but even with very long chains (up to 1000 classes) the impact to the deploy times was not large enough to explain what we see with package code.
Use the trees
Having failed to identify a cause looking at invalidation the next step was to look at class dependants as logically this is the inverse, so if Class A calls a method on Class B we might say updating B invalidates A or equivalently A depends on B. To test dependencies I created various sizes of binary tree using classes to get this result.
The simplest 1-layer tree consisted of a root node class that calls methods in two other classes. In a 2-layer tree the root node class calls methods in two other classes but each of those calls methods in two more classes. If you run this test with no other code in each class then you can see an increase in deploy times but its not that clear. I found I could create the result above by adding additional code to each class that only depends on platform types.
What this graph is showing is a near linear increase in the time to update the root class of the tree as the tree size grows. There could be a few reasons for this so time to dig a bit deeper with another experiment.
For this test I added ‘weight’ to the classes by using a number of identical small blocks of code . This means that when the number of classes doubles I could half the number of code blocks to keep the overall amount of Apex code in the tree about constant.
What the data is showing is that for small->medium class numbers the time to update the root class of the tree is proportional to amount of code in the class and all dependent classes.
With high numbers of classes we can see that that deploy times start getting worse but it’s not the linear relationship we saw earlier, so it’s a smaller factor to consider than just the amount of code in all dependent classes.
Back to invalidation
With the Salesforce Apex runtime being essentially a black box understanding why something happens as apposed to what happens can be very difficult if it’s not documented. In this case I am going to speculate a bit and try and explain why we might be seeing this behaviour.
The invalidation result earlier got me thinking about what kind of data structure that could be being used to invalidate classes very quickly. In this context I think it might be important to understand that class references in an org support some very dynamic behaviours. For example, I can replace a platform class with my own class of the same name (don’t do this it’s a horrible anti-pattern). If I did do this how are existing class references handled?
My guess is (and that’s all this is) is that class references are always being resolved during class loading. This would mean the only way to perform quick invalidation would be to store a list of classes names that should be invalidated with each class so that when it is updated there is no need to analyse class dependencies. If that is the case then when you update a class you may need to recompute an invalidation list for all the dependent classes by analysing the code in the dependency tree.