Solving the “No logical space left to create more user strings” Error and Improving performance of Pre-generated Views in Visual Studio .NET4 Entity Framework

As shown in the previous precompiled view blog (Isolating Performance with Precompiled/Pre-generated Views in the Entity Framework 4), views can significantly improve performance in various scenarios such as first-run; memory usage; and execution of one-off queries. However, the creation of views for large complex models that consist of many entities and associations), can create lots of views which in turn will require heavy use of strings, and reach the .NET metadata format limit on the number of user string characters (0xFFFFFF). Hence when trying to compile a project leveraging this type of views, the following compile error will occurred.

Error 1 Unexpected error writing metadata to file 'D:\Compile view fix\...\PerfSample.exe' -- 'No logical space left to create more user strings.'

For this reason, the following revisits this implementation and does two things, demonstrates how to further increase pre-compiled view performance and, at the same time, avoids the error on large complex models.

For ease of reading, it is recommended to review the previous post on pre-generated views.

Analysis

Looking at the generated code inside the pre-compiled view file, which by convention is named [projectName].views.cs; as seen below, one notices that in essence the code takes a number (an index) and with it searches thorough a large set of conditional statements to return the correct key value pair in a method.

Each method then returns the key value pair which consists of a set of strings, one for the key and another for the value, as seen here.

Design Approach

Here is where the number of user strings characters can reach the compiled limits. This is what needs to be avoided. For such, a couple of improvements will help.

First, instead of using plain strings, the code can reference a separate resource file and that way it will avoid reaching the compile limits on user strings.

Second, instead of IF statements, a dictionary that will return the required key value pairs will also improve efficiency.

To achieve this, a standalone application is needed. It will take the original view file and based on it, create a new pre-compiled view file that will reference a newly created resource file, as follows.

The code for this helper application can be downloaded from here; it is a console application that will take as the only parameter the EF pre-generated view file. Once it is done, it will create the two following files. Notices the simplification of the method returning the key value pair and how the resource file stores the strings as a key value pair in its raw .NET binary form.

WihtDictionary_PerfSample.views.cs

   

WihtDictionary.views.cs.resources

To use these files, first one needs to have a working EF solution which already implements a pre-generated view. Then, after running the above tool, the new dictionary pre-generated view file needs to be moved and renamed to replace the original EF created view file, and the resource file will need to be placed beside your executable file (in the case of a visual studio solution, it will be in the release or debug directory)

NOTE: as experienced, each time an EF generated pre-compiled view is replaced with a Dictionary pre-compiled view, the Visual Studio solution needs to be clear (From the tool bar: Build and then Clean Solution) before it is rebuilt.

The implementation

At run time, the new view file which implements a generic IDictionary, will get populated at the point where the class is instantiated (its constructor) by using the key value strings found in the new resource file. See below.

The new generated view file is fairly similar to the original file: namespace, class name, method names, as well as the hard coded hash values which EF uses as reference to check that the view strings have not been modified. This last point means that the strings being stored in the resource file will need to be exactly the same as those extracted from the original generated view file (no extra or missing spaces or newlines nor commas…).

Everything else will remain exactly the same: the CSDL, MSDL and SSDL as well as the ObjectLayer and App.config files will not require any changes since the fact that a dictionary and a resource file will be used instead of the conditional statements should be completely transparent to all other “moving parts”.

Testing the theory

So far this seems fairly believable but what gain should be expected? This will of course vary on the characteristics of the model and the underlying database but by leveraging the methods described in the blog on Isolating Performance with Precompiled/Pre-generated Views in the Entity Framework 4, and gathering results for both the original views and the dictionary views, the following results can be observed.

NOTE: For large models, the use of T4 templates for view generation may be very slow and may crash Visual Studio; therefore the use of EDMGEN will be more appropriate.

(*) Total entities

With original pre-compiled views

With dictionary pre-compiled views

100

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 2,094 | 63,224,212 |

| 564 | 54,347,924 |

| 651 | 54,352,824 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 1,993 | 64,264,668 |

| 677 | 53,303,412 |

| 658 | 54,348,216 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 1,976 | 63,224,084 |

| 594 | 54,347,796 |

| 600 | 54,353,592 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 1,998 | 61,385,776 |

| 579 | 53,299,864 |

| 605 | 53,303,996 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 2,076 | 61,384,496 |

| 684 | 53,299,864 |

| 571 | 53,299,388 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 1,911 | 61,383,728 |

| 628 | 53,299,992 |

| 616 | 53,299,516 |

300

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 4,631 | 81,378,508 |

| 645 | 53,299,956 |

| 803 | 53,304,344 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 4,585 | 81,377,100 |

| 694 | 53,299,956 |

| 777 | 53,304,344 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 4,426 | 81,373,388 |

| 854 | 53,304,052 |

| 658 | 53,300,248 |    

 ...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 4,401 | 75,376,604 |

| 755 | 53,304,052 |

| 797 | 53,300,248 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 3,984 | 76,426,788 |

| 804 | 54,351,544 |

| 762 | 53,300,248 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 4,114 | 76,428,708 |

| 783 | 54,347,960 |

| 742 | 53,304,344 |

500

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 6,933 |101,150,740 |

| 578 | 55,274,548 |

| 551 | 55,266,648 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 6,678 |101,145,472 |

| 606 | 55,274,548 |

| 534 | 55,266,648 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 7,054 |101,150,724 |

| 642 | 55,274,420 |

| 566 | 55,266,648 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 6,168 | 90,127,776 |

| 651 | 55,270,416 |

| 579 | 55,270,708 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 6,411 | 90,127,776 |

| 554 | 55,270,416 |

| 761 | 55,270,708 |

...\bin\Release>call PerfSample 

| Time MilSec |Manage Bytes |

|---------------------------|

| 6,327 | 90,127,776 |

| 717 | 55,270,416 |

| 732 | 55,270,708 |

700

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 14,051 |117,458,568 |

| 828 | 55,267,344 |

| 770 | 55,267,636 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 12,254 |119,724,760 |

| 786 | 55,271,476 |

| 805 | 55,271,768 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 12,795 |119,721,560 |

| 888 | 55,271,476 |

| 785 | 55,271,768 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 10,576 |104,291,448 |

| 656 | 55,271,348 |

| 764 | 55,271,640 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 9,875 |104,291,960 |

| 766 | 55,271,476 |

| 771 | 55,271,768 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 10,103 |104,295,160 |

| 731 | 55,271,348 |

| 795 | 55,271,640 |

1000

 

 

 

 

 

 

 

Not applicable, build error:

'No logical space left to create more user strings.'

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 14,708 |124,840,944 |

| 539 | 55,273,140 |

| 543 | 55,273,432 | 

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 11,214 |124,841,584 |

| 633 | 55,273,140 |

| 657 | 55,273,432 |

...\bin\Release>call PerfSample

| Time MilSec |Manage Bytes |

|---------------------------|

| 11,239 |124,841,584 |

| 594 | 55,273,140 |

| 627 | 55,273,432 |

(*) Note that this is only a reference number, the model been created are very simple and therefore should only be used as reference and not as a close approximation of what a real-life model maybe. Running similar tests against the implemented models is recommended.

This isolated tests show that the improvements are more apparent with larger models. For the 1st run the gains in time are modest, 100 and 300 entities are about 1% ~ 3% faster, the memory usage is between 3% ~ 5% less too. At 500 entities the 1st time runs gains are noticeable, in both time and memory, at around 8% ~ 10%, and subsequent runs are very uneven likely due to other types of cache (server RAM, SQL cache…) taking effect. The real gains are seen at 700 entities, where the 1st runs were 25%, 20% and 37% faster in time to completion and memory usage drops by about 12%, and time lapse on subsequent runs are now a more even, where dictionary pre-compiled views are around 7% faster. The true difference is obviously at 1000 entities, since this method becomes the only way views can be utilized.

Conclusion

The use of a dictionary is creating a first time cache like behavior and hence the 1st load runs are considerably faster (over 25% in cases with large amount of models), in subsequent runs other types of cache (server RAM, SQL cache,…) take precedence but gains of 7% can be seen in large models. The method of using a resource file helps with memory management while processing the same amount of large strings and as such, the usage of managed memory drops on all tests.

The larger your model, the more reason to implement this method, as all the analyzed data points show improvement. In the cases where the compiled error is discovered, then this becomes the only method to work around the issue. As always, first do in-house tests before full production implementation.

Authored by: Jaime Alva Bravo

Comments

  • Anonymous
    August 01, 2011
    Jamie, This is good stuff, the only problem is that it appears that EdmGen doesn't spit out consistent views.  I have some views in my view file that do not use a string builder but just build the string directly in the second part of the key value pair constructor.There are two variations on this as well:new System.Collections.Generic.KeyValuePair<string, string>("DomainModelStoreContainer.PARTYRELATIONSHIP", "rn    SELECT VALUEnew System.Collections.Generic.KeyValuePair<string, string>("DomainModelStoreContainer.PERMISSION", @"   SELECT VALUE -- Constructing PERMISSIONI've spent the past couple days trying to catch all of the variations and nuances that can happen but have been unable to get EF to accept my views.  We're very close to getting this error right now (over 700 views and most of them not trivial), and separating the model is not something we can do without significant cost.  Any suggestions would be appreciated.