1

I have a SQLCLR assembly that does a simple JSON deserialization using the LitJson package on a SQL Azure Managed Instance. This CLR is called from a table-valued function that just returns the JSON properties as a table (in theory faster than the built-in JSON handling in T-SQL).

The weird thing is that the assembly runs much faster when unloaded (i.e. when it doesn't show up in sys.dm_clr_loaded_assemblies) than when it is loaded. For some color, I can deserialize 1,000 records in ~200ms when it is unloaded, and the same 1,000 records take ~7 seconds when the assembly is loaded.

I have a workaround, which is that at the beginning of my query I toggle the PERMISSION_SET back and forth from UNSAFE to EXTERNAL_ACCESS which forces an unload of the assembly, but this feels like a hack. The assembly should be faster loaded than unloaded.

Any thoughts here would be greatly appreciated. The code is sketched out below -- nothing fancy going on there at all.

[SqlFunction(FillRowMethodName = "FillRowMessageParser", IsDeterministic = true)]
    public static IEnumerable ParseRows(string MsgText)
    {
        DatabaseRow[] myRows;
        //LitJson doing its work here 
        myRows= JsonMapper.ToObject<DatabaseRow[]>(MsgText);

        return myRows;
    }

    public static FillRowMessageParser(object obj, out SqlChars Field1, out SqlChars Field2, [bunch more out fields here])
    {
         var myRow = (DatabaseRow)obj;

         //Set a bunch of fields to the out variables here
         Field1 = new SqlChars(myRow.Property1);
         //whole bunch more here

         //loop through some nested properties of the myRow class
         foreach (var x in myRow.Object1)
         {
              switch(x.Name)
              {
                 case "1": Field2 = new SqlChars(x.Value); break;
                 //whole bunch more here
              }
         }
    }

The SQL component looks something like this:


DECLARE @JSON NVARCHAR(MAX) = 
(
SELECT 
    TOP 1000
        MessageID,
        JSON_QUERY(MessageText) AS MessageText
FROM MyTable
ORDER BY 1 ASC
FOR JSON AUTO
)

DECLARE @Start DATETIME2
DECLARE @End DATETIME2

SET @Start = SYSDATETIME()

SELECT *
FROM MyCLRTableValuedFunction(@JSON)

SET @End = SYSDATETIME()

SELECT DATEDIFF(MILLISECOND,@Start, @End) --Time CLR takes to process



UPDATE

It appears the issue has to do with the LitJson package itself. We ended up trying JsonFx as another package that does not require any unsupported SQL Server .NET libraries (shoutout to @SolomonRudzky for the suggestion), but for whatever reason the performance of that package in deserialization, which is what our exercise is about, wasn't as good as the native T-SQL JSON handling (at least for our dataset). So we ended up moving off SQLCLR and back to T-SQL for this process. The performance in T-SQL still isn't as good as the unloaded LitJson package, but its good enough for our needs and avoids too many wonky workarounds with unloading the assembly on every call to the CLR.

Solomon Rutzky
  • 46,688
  • 9
  • 128
  • 171
Jon
  • 91
  • 1
  • 7
  • I haven't fully reviewed the LitJSON code, but it's possible that it's using the readonly static class variables to store state between calls and hence in the first call those collections are empty, but upon additional runs they have data in them? Can you execute it once to load it but only pass in 1 item, then run it again for the 1000 items and see how that impacts performance? – Solomon Rutzky Jun 03 '20 at 00:25
  • The above doesn't make any sense at all, no, not you - but the behavior :). I agree with @SolomonRutzky, to run it from unloaded state with one row, and after it's been loaded for 1000. – Niels Berglund Jun 03 '20 at 03:50
  • Thanks to the both of you. I did indeed try what @SolomonRutzky suggested, which was to unload via the ALTER statement (which btw I found on StackOverflow from one of your posts Solomon, so thank you for that!), then make a call to the CLR with 1 record, then follow it up with the 1000 records, but the 1000 continues to be slow (~7 seconds consistently). When I add the ALTER statements to the top of the query, it is consistently fast (~200-300ms). – Jon Jun 03 '20 at 13:12
  • A problem with the workaround that I just discovered today is that the unload disturbs other CLRs in the same appdomain, which has caused periodic crashes while other CLR runs are in-flight, so I am not so sure if it will even work as a long-term solution. Its just such a weird problem, at this point I have to find the answer as a matter of principle. – Jon Jun 03 '20 at 15:53
  • Interesting. This might still be related to static class variables. Nothing else sticks around between calls. When you say "_~7 seconds consistently_", do you mean that you execute this several times in a row, and the first execution is fast, but executions 2 - n are all approx. 7 secs? Also, what `PERMISSION_SET` does this assembly usually use? Finally, regarding the app domain issue for other assemblies when changing the permissions of this one: that's easy to solve, just create a user `WITHOUT LOGIN` and change authorization of this assembly to that user so it will have its own app domain. – Solomon Rutzky Jun 03 '20 at 15:59
  • Re: the "~7 seconds consistenty" part, yes you have it right. The first execution (or maybe more precisely, any execution where the assembly is not loaded and visible in `sys.dm_clr_loaded_assemblies`) is fast, but any executions where the assembly is loaded seem to be slow (~7 seconds per 1,000 JSON documents). Re: the `PERMISSION_SET`, it is `UNSAFE` as it appears LitJson has some requirement there (the error I get when set to `EXTERNAL_ACCESS` is "The protected resources (only available with full trust) were: All The demanded resources were: Synchronization, ExternalThreading" – Jon Jun 03 '20 at 16:35
  • As for the app domain, I have a few different CLRs that all have LitJson as a dependency, and SQL Server doesn't seem to like when I have different owners throughout a dependency chain. I suppose I could load multiple versions of the LitJson assembly to partition into different domains, if I have to go there. But not sure (haven't tried) if I can even have different assemblies with the exact same signature. – Jon Jun 03 '20 at 16:37
  • Jon: No, you can't have multiple copies of an identical assembly loaded into a single DB, even if they have different owners. I just tried and it still compares the MVID. The "_Synchronization, ExternalThreading_" message might get us closer to finding the issue. Again, I don't have time to review their code, but this might have problems if used outside of a single-threaded process. (continuted) – Solomon Rutzky Jun 03 '20 at 17:02
  • App Domains in SQL Server are shared across all sessions, which is why it doesn't like non-readonly static variables: they are shared between sessions and this leads to race conditions, unless the values being cached don't change between sessions. I saw at least 2 readonly static collections, and those can have elements added to / removed from them. You need to review their code to see how they are using these things as this code might not be safe to use for your purposes. Also, please use someone's `@` name when replying so they get a notification now that more than 1 non-OP has commented :). – Solomon Rutzky Jun 03 '20 at 17:04
  • Thanks @SolomonRutzky, appreciate the advice. I will do some more digging on that front on my side. – Jon Jun 03 '20 at 17:07
  • Sounds good. Good luck. And just FYI: in comments, the poster of the question of answer (whatever the comment is on) always get notifications. If there is only 1 non-OP in comments on that question/answer then they automatically get notified, but as soon as one other non-OP comments, then none of the non-OP folk get auto-notified. That's why I don't need to use your `@` name here, but now that @NielsBerglund commented, you need to use our `@` names to notify us, _and_ you can only do one per comment :-( – Solomon Rutzky Jun 03 '20 at 17:12
  • One more thought @SolomonRutzky, the only reason I am using LitJson was because it was the only package I could find that handled Json deserialization without any unsupported SQL Server .NET dependencies (we have already had troubles with the Newtonsoft package in the past because for this). If you know of any Json handlers that can work on the supported SQL Server .NET library, I am all ears! And thanks for all the advice, much appreciated! When I started writing this, I was hoping I would catch your attention as you seem to be the most knowledgeable SO poster when it comes to SQLCLR! – Jon Jun 03 '20 at 17:16
  • Well, thank you for that compliment . I think I looked at LitJSON years ago but decided against using it for my [SQL#](https://sqlsharp.com/) project for some reason. I ended up going with JsonFx instead. Of course, that code also had non-readonly static variables, but I went through and made them readonly _and_ I believe they weren't collections and so couldn't be modified post initialization (else compilation would have failed). (continued) – Solomon Rutzky Jun 03 '20 at 17:31
  • So far I only use it to convert from JSON to XML as XML can be parsed natively, but I could add a proc to return a table (dynamic columns) assuming a flat JSON document. And this has been in the plans for years anyway (this just gives me a reason to finally do it). But first, you said "_in theory faster than the built-in JSON handling in T-SQL_". So you haven't tried the native JSON stuff? You should. What version of SQL Server are you on? – Solomon Rutzky Jun 03 '20 at 17:33
  • @SolomonRutzky we are using SQL Azure Managed Instance, which in general we are very happy with. As for the native T-SQL JSON stuff, I originally implemented this process with that, but found the performance to be somewhat lacking. At that time, we were using Newtonsoft until it got nuked when Azure updated the .NET on that instance, which made us realize that trying to use unsupported .NET libraries on a machine we don't control was a bad idea. (continued) – Jon Jun 03 '20 at 17:36
  • @SolomonRutzky So the idea was that this "flattening" of JSON into a table was a perfect candidate for CLR for potentially speedier implementation than T-SQL. As I recall, these same 1,000 records took ~ 1 second to flatten, and we can do ~5x better with the unloaded LitJson. In just a cursory overview of the T-SQL JSON stuff, it seemed to me to be much much faster at building JSON documents from tables than converting JSON documents back into tabular form, especially if there were lots of columns/JSON properties (as we have). – Jon Jun 03 '20 at 17:40
  • @SolomonRutzky, sorry, the above was not clear. We tried T-SQL, wasn't great, then tried Newtonsoft, but had been broken by .NET update, then tried LitJson and here we are :) – Jon Jun 03 '20 at 17:43
  • Yes, I understood what you were saying about native -> Json.NET -> LitJson. In fact, I never incorporated Json.NET into [SQL#](https://sqlsharp.com/) due to all of the external dependencies, as much as I wanted to due to its popularity and capabilities. Lemme see what I can do with JsonFx in SQL#. It's been too long since I have updated it so it's due for at least a minor update. – Solomon Rutzky Jun 03 '20 at 17:45
  • @SolomonRutzky Cool -- I will say, a big part of what makes LitJson useful for us (and the Newtonsoft package also, for that matter) is that we do have a nested class structure in our JSON, and the magic that happens in their .ToObject() method to map all that nested structure over into classes is what makes it oh so convenient. – Jon Jun 03 '20 at 17:49
  • @Jon Hmm.. I might need more info, but not sure that this is the appropriate place for that. Perhaps you can go to the contact page on my [blog](https://sqlquantumleap.com/) and send me some details on the specific need(s). Thanks. Either that or you can check out JsonFx on your own. – Solomon Rutzky Jun 03 '20 at 18:18
  • @SolomonRutzky, Thanks I will reach out. For anyone else reading, I swapped LitJson for JsonFx, but unfortunately the performance there doesn't seem to be great out of the box (note we didnt modify the properties `readonly`, just marked `UNSAFE` to try it out). Those same 1,000 rows take ~2 seconds to process, whether the assembly is loaded or not. At least that I can wrap my head around! – Jon Jun 03 '20 at 18:58

1 Answers1

0

While I cannot provide a definitive answer at the moment due to not having time to fully review the LitJSON code, I did look over it briefly and am guessing that this odd behavior is the result of using static class variables (mostly collections) to cache values during processing. I can't think of anything else that would different from the first execution to subsequent runs outside of:

  1. Assembly isn't already loaded into memory on the first run
  2. Static class variables are initialized but likely don't contain any values during execution (unless initialization loads data that is simply read on all executions)

Doing such things does usually improve performance, but there is a nuance when doing such things in SQLCLR: AppDomains in SQL Server are shared across sessions. This means that shared resources are not thread safe. This is why typically (i.e. outside of marking the assembly as UNSAFE) you are not allowed to use writable static class variables (you will get an error saying that they need to be marked readonly). However, in this particular case, there are two breakdowns of this rule that I see:

  1. changes made in 2015 (and merged 2 full years later) seem to indicate that the desire was to get LitJSON to work in an assembly marked as SAFE, hence all static class variables were marked as readonly, and additional changes were made to accommodate this. you can see those changes here: https://github.com/LitJSON/litjson/commit/1a120dff7e9b677633bc568322fa065c9dfe4bb8 Unfortunately, even with those changes, even if it did "work" in a SAFE assembly, the variables are still static and are hence still shared. For some technical reason it is permitted to add/remove items from a readonly collection, so on a practical level, they aren't truly read-only. This can definitely lead to unexpected "odd" behavior.
  2. If the intention of the changes mentioned above were to allow the assembly to work while being marked as SAFE, then clearly something has changed since that SQLCLR-based commit 4.5 years ago given that not marking it as UNSAFE now results in the following error (according to the OP):

    The protected resources (only available with full trust) were: All The demanded resources were: Synchronization, ExternalThreading So, currently the code requires being marked as UNSAFE, in which case, none of the changes made to mark the static class variables as readonly were necessary ;-).

Regardless, I don't think this code is thread safe. In fact, you might be able to see "odd" behavior by doing multiple executions, each one with a different JSON document that varies in structure (at least number of elements).

Again, this is not definitive, but is more likely than not. In which case, I'm guessing that the great performance of the first execution is due to the code doing things that would not actually work in production. Of course, you do have a hard-coded structure (the output row schema is compiled into the code) so I suppose that eliminates the case of passing in different structures, but it's still not clear what the effect would be if two sessions execute this with different JSON documents at the exact same millisecond.

Solomon Rutzky
  • 46,688
  • 9
  • 128
  • 171
  • Thanks Solomon, I appreciate the leg work you did here to review this. I agree that the issue is with LitJson and the moral (at least for me) is that without modifications it is unsuitable for reference in a CLR. I have marked your answer as correct, thanks again! – Jon Jun 05 '20 at 20:27