Sunday 19 June 2016

Retail Category performance problems and a new way of thinking

Hi all. It's been a while since I blogged and like all of us, our precious time means that, on rare occasions we are compelled to share in the hope of exchanging knowledge. Hopefully the solution and investigation below will help many a fellow AX developer and architect in more ways than one.

Cutting a long story short, we discovered an issue in editing Retail Hierarchies (EcoResCategory), which interacts with RetailCategoryContainmentLookup (RCCL). I wanted to publish the solution for many reasons, detailed below, but more so, I strongly felt compelled to explain the need for designing elegant solutions and minimizing the impact on the system
  1. First reason is to share the solution, exiting any user form should take no more than a couple of seconds, no matter what the change, or users get concerned.
  2. Solving any problem should involve thinking about the wider issues, not just the one at hand, this is the second principle of innovation and all code is about creating something innovative. The routine RetailCategoryContainmentLookup::synchronizeAllRetailCategories  deletes all records. Rebuilding a table of a 100k entries is unnecessarily long and introduces a secondary issue where the table is empty for what seems line an eon, so in designing any code routine ensure you are working with the smallest possible changes.
  3. When handling multiple records, start thinking about how to work in small sets of records. Unsurprisingly, rebuilding the missing 1000 or 10000 entries can be slow if you process each record one at a time. But also the first code fix we saw was attempting a set operation by a mass cross joins and group, instead of handling the problem as a set of smaller steps.
  4. Ultimately there is many ways to solve this by using the most appropriate technology, whether SQL or X++ to ensure you meet just the right requirements.

For those wondering, the aforementioned routine in AX 2012 R3 CU 10 used to take a few minutes even for a 1500 lines in EcoResCategory, not exactly what you expect from closing a form. Microsoft has published a solution KB2992401 which reduces this to 2 minutes but still deletes all entries. Our customer has 90k records and even though the KB reduces this from 45 minutes to 10 minutes the X++ solution I am about to talk about reduces this to 3 seconds (a full rebuild in 18 seconds).

The optimal solution for our requirements is at the bottom :) read on if you want to understand my thinking and have time to understand more than just enclosed X++ code. More than happy to guide you if you contact me. Please also note the usual disclaimer in using any of the snippets below as this is not a deployable solution nor intended to be so.

First some background


Our current client has a rather complex retail hierarchy (EcoResCategory) and this is unique to each branch of their business. So in fact, we will end up with a table of over 90,000 entries. But our problems started a lot earlier than that with only 6,000 entries. Simply put when editing a hierarchy within the EcoResCategory form and closing this form we end up with very long delay (minutes). This causes the destruction and recreation of the RetailCategoryContainmentLookup (RCCL) table, which also introduces our second problem in that whilst this is being rebuilt we get errors in routines which we constructed to lookup values using this table. We create a view to flatten the RetailCategoryContainmentLookup (RCCL) for 3rd level lookups which fail during this time.

A little bit of background, the EcoResCategory is in fact a hierarchical tree, for example
  • Store 1 (parent 0)
    • links to Business Unit 1 (Parent Store 1)
      •     which in turn links to Department 1.1 (Parent Business Unit 1)
      •     and also links to Department 1.2 (Parent Business Unit 1)
    • A second Business Unit 2 (Parent Store 1)
      • links to a new Department 2.1 (Parent Business Unit 2)
The 6 records are then expanded into the RetailCategoryContainmentLookup to show all the parent child pairs as follows (I will tell you in a little while how I discovered this)

  • Store 1 - Store 1
  • Store 1 - Business unit 1
  • Business unit 1 - Business unit 1
  • Store 1 - Department 1.1
  • Business Unit 1 - Department 1.1 
  • Department 1.1 - Department 1.1 
  • Store 1 - Department 1.2
  • Business Unit 1 - Department 1.2
  • Department 1.2 - Department 1.2 
  • Store 1 - Business unit 2
  • Business unit 2 - Business unit 2
  • Store 1 - Department 2.1
  • Business Unit 2 - Department 2.1
  • Department 2.1 - Department 2 .1

Begin with understanding and the test scenario first


At first glance RCCL looked like it had tree pairs of records, but no easy way to confirm this. The code is complex and looked like a puzzle. In ensuring we had the correct solution I deduced that each EcoResCategory record needs as many RCCL records as its level, it was a mathematical equation, like in my school days (who says maths does not come in handy). So a leaf at level 3 needs exactly 3 records. This is both an easy test as well as a invalidation method for dirty records.

After confirming that the total RCCL records we need, is the same as the sum(level) from EcoResCategory, it was now abundantly clear that the RCCL table is a recursive tree. Although this is slightly lengthy to solve in X++, SQL has an easy way to achieve this. And out of the blue I had both a test method and the first method for creating RCCL records

with RetailCategoryCTE (category, lvl1, containedcategory, lvl2) as
(select e.recid, e.level_, e.recid, e.Level_ from ECORESCATEGORY e
join ECORESCATEGORYHIERARCHYROLE h
on h.CATEGORYHIERARCHY = e.CATEGORYHIERARCHY
where e.ISACTIVE = 1 and h.NAMEDCATEGORYHIERARCHYROLE in (4,20,22)
union all
select a.category, a.lvl1, b.recid, b.Level_ from RetailCategoryCTE a
join ecorescategory b on a.containedcategory = b.PARENTCATEGORY
and ISACTIVE = 1 and b.PARENTCATEGORY > 0)
select * from RetailCategoryCTE c
left outer join RETAILCATEGORYCONTAINMENTLOOKUP l
on c.category = l.CATEGORY and c.containedcategory = l.CONTAINEDCATEGORY
where l.CATEGORY is null
union
select * from RetailCategoryCTE c
right outer join RETAILCATEGORYCONTAINMENTLOOKUP l
on c.category = l.CATEGORY and c.containedcategory = l.CONTAINEDCATEGORY
where c.CATEGORY is null

In determining a way to test this I now had a solution in SQL which at low record counts (9000 or less) is instant, as well as I gained deeper understanding of the structure of the records.

Solution 1: using SQL


As a solution, we can simply destroy the whole table and reconstruct it, by creating a Pre Event handler to the RCCL method. We then create a set of stub records within AX X++ code and then send the harder problem to SQL.

Constructing the stub inside AX, by simply looping for all the possible levels, so first find the maximum number of levels in EcoResCategory for the appropriate hierarchyRoles (see original method for this)
select firstonly level from ecoRedCategory order by level desc 
 Secondly create the record stubs in X++ which creates RecIds and the necessary pairs
for (levelLoop = 1; levelLoop <= maxlevel; levelLoop += 1)
{
    insert_recordset containmentLookup (category, containedcategory, isdirty)
    select levelLoop, recid, SetNo
    from category where category.level >= levelLoop;
}
You can then use an executeUpdate query which looks very much like the above test query and updates the category field.

This took a couple of seconds to process but unnecessarily deletes all records first.

Please also note that there is a tertiary set of tables involved which needs to be accounted for RetailSpecialCategoryMember and RetailGroupMemberLine (see notes later)

Solution 2: Start by deleting only necessary records

First of all the records in RCCL table are not deleted but set to isdirty. So lets apply certain rules to dirty any necessary records, as follows
  • First dirty any records in RCCL which do not have the equivalent RecID in EcoResCategory
  • Secondly mark any records where the pair no longer exists in EcoResCategory
update_recordSet RCCLNewsetting isDirty = setYes
exists join ERC where (RCCLNew.ContainedCategory == ERC.RecId) && ERC.ParentCategory != 0
notexists join RCCL
where RCCL.ContainedCategory == ECR.RecId
&& RCCL.Category == ERC.ParentCategory
&& RCCL.IsDirty == setNo && RCCL.Processed == setYes;
 
  •  Repeat the same update above for the RCCLnew.Category field. Lesson learned here, combining the above statement to join for both (RCCLnew.Category || RCCLNew.ContainedCategory) against ERC.RecId gives you significantly slower results. So always check your execution with full data and use trace parser to better understand your code.
  •  Mark all records where the total number of recs for RCCL.ContainedCategory is not the same as the Level field in EcoResCategory. I used a View based on a Query which groups the RCCL records by ContainedCategory and provides the total number of records
Finally delete all records where the RCCL.ContainedCategory is dirty for any records at all

delete_from RCCLNew
exists join RCCL
where RCCLNew.ContainedCategory == RCCL.ContainedCategory
&& (RCCL.IsDirty == setYes || RCCL.Processed == setNo);

You may have spotted already a new field in RCCL called processed. This is being used to process only the necessary records.

Solution 2. Finish by creating the necessary records

Now we cleaned up by only deleting the dirty records and any records associated with these records, we begin by reconstructing all necessary missing records starting from the bottom up.

Unsurprisingly we first add into the RCCL table all records, for the appropriate category roles, which do not currently exist in RCCL. Setting the isDirty to No and of course the Processed to No. The Category and ContainmentCategory are set to the EcoResCategory Recid
insert_recordset containmentNew
(category, containedcategory, isdirty, processed)
select
recid, recid, setNo, setNo
from category
exists join hierarchyRole
where category.CategoryHierarchy == hierarchyRole.CategoryHierarchy
&& (hierarchyRole.NamedCategoryHierarchyRole == EcoResCategoryNamedHierarchyRole::Retail
|| hierarchyRole.NamedCategoryHierarchyRole == EcoResCategoryNamedHierarchyRole::RetailSpecialGroup
|| hierarchyRole.NamedCategoryHierarchyRole == EcoResCategoryNamedHierarchyRole::RetailChannelNavigation)
notexists join containmentLookup
where containmentLookup.Category == category.RecId
&& containmentLookup.ContainedCategory == category.RecId;

So now we keep looping until we have nothing else to process and first we add the parent records to the records currently not processed, which explain the new indicator

insert_recordset containmentNew
(containedcategory, isdirty, processed, category)
select
containedcategory, setYes, setNo
from containmentLookup
where containmentLookup.processed == setNo
join ParentCategory from category
//If the category field has a parent then this needs to be created
where category.recid == containmentLookup.category
&& category.ParentCategory != 0;
We can also now handle the inserting of records for RetailSpecialCategoryMember and RetailGroupMemberLine

Mark all records which are not dirty and not processed as processed. And finally mark all dirty records as not dirty, This will basically leave you with records which were not processed and not dirty for the next round of processing.

Repeat these steps until you have no more records.

Solution 2 final conclusions.

Is it in fact possible to look to simplify the problem by using a combination of SQL CTE returning a full table and then using this to compare and contrast, but in all my working it seems as fast to handle the situation in X++ with comments and selects which most X++ developers understand, unless we are dealing with much larger sets of records.

Two key techniques helped in deterring the best solution, first being able to test the solution using the SQL CTE query and secondly trace parser. It became easily visible that in fact the RCCL table had an index which only contained the isDirty flag. This caused over  3 second delay in my code and I removed it as its a completely pointless non specific, non unique and in fact poor index as its non-sequential. Records will switch between isDirty and cause unnecessary page breaks in SQL so this is a clear target for performance elimination :)

Conclusion is that by breaking the problem down to small enough chunks and using simply set operations is much more efficient and faster (<1 second) than trying to solve the problem in one big step (2 minutes) or in  many individual steps (20 minutes).
 
For most folks taking the KB from Microsoft may suffice, after all use the path of least resistance.

If you need something more timely then feel free to borrow from the above and contact me in your endeavors.

Keep enjoying the learning process
Angelos


Monday 9 June 2014

Understanding and optimisng your SSD for maximum performance

 
Over the past few months I have optimised machines using newly purchased SSD and had to pass on this information to others. This is a consolidation of a few sources of information which hopefully you will find handy. It has been tried and tested but I do not accept any responsibility if you have luck or even bad luck with it. I ended up purchasing Samsung SSDs because of their capacity and the latest technology they were brining out, so you may need to interpret this for other SSD manufacturers, but most of the points below hold true no-matter the make of the SSD.
 
As with everything, please do the same as I did, research first test second, share third :)
 
   
Firstly it is worth while getting hold of the SAMSUNG magician software which tells you about a number of things you should be doing, if you don’t have the background already to configuring your SSD. It also has an additional advantage in that it can let you measure your disks performance without needing any other hardware
 
  
Starting from the PC architecture
 
  1. SSDs can capitalise on the latest AHCI mode over the older IDE storage communication mode http://www.diffen.com/difference/AHCI_vs_IDE. This will boost your read and write performance of your SSD from 10% to 20% (or higher for some machines).
    • CAUTION: If you have already installed Windows with a normal HDD, you need to edit your registry beforehand to ensure you machine will boot up if you change the AHCI setting in your BIOS - the instructions below are for Windows 8 and other operating systems will vary
    • HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\storahci\
      • "Start" value should be set to "0" (Default is 3)
      • "ErrorControl" value should be set to "0" (default is 3)
      • "ImagePath" should contain "System32\drivers\storeahci.sys" and check that this file exists
    • HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\storahci\StartOverride
      • "0" value should be set to data value of "3"
      • Sometimes this entry is listed as "1" not "0" but this should still be the value to change 
    • Please be aware that his is not a 100% guaranteed step, and you next alterative is to re-install Windows from scratch one you have AHCI turned on
  2. Update your drivers which should boost you disk significantly and stop any false readings (on some machines the IO is reported as running @ 10% due to out of date drivers) and especially the "Intel Rapid Storage Technology" if you can get hold of it for your machines (Released 14/10/2012)
    • Please ensure you go to your computer manufacturers support page and select the correct operating system before downloading the appropriate Serial ATA drivers.
  3. Ensure you are running on SATA 3 and not SATA 2 as for example Dell 9010 machine only have 2 SATA 3 ports one of which is connected to the DVD and the second to the shipped HDD. This can give you a spead increase of up to 100% performance
  4. Firmware updates are also important and for Samsung SSDs very easy to do.
    • CAUTION: If a firmware update signifies it needs you to take a backup, then please do so.
  5. Performance optimisation. A big misunderstanding exists behind disk defragmentation and SSDs. IN windows 8 the disk defragmenter is now called "Disk Optimiser". Both traditional HDDs and now SSDs need to be optimised which either moves files to eliminate fragmentation for HDDs and will clean up already used space on SSDs to ensure you can write to them faster. Please ensure you set a schedule in the Disk optimisers to optimise all your disks.
    • Please note that SSDs must also write on clean space which means that they will have to clean up the space before writing to it, which costs two cycles as opposed to one. Hence disk optimisation simple truncates and wipes the space before hand.
  6. Over Provisioning works in conjunction with disk optimisation but it also gives you one more advantage which you cannot gain by other means. It gives you
    • Faster overall write IOPS and
    • Better reliability with longer endurance of the SSD
      1. CAUTION: Do this early on in the use of the disk or it can fail to happen afterwards.
      2. All SSDs have a maximum number of writes per disk cell. So after that point the SSD cells will fail. To minimise this the SSD tries to spread all its writes evenly across the whole disk but as you can imagine this is a costly process, especially with small changes. Over provisioning allows you to keep some space free to both avoid the overhead of cleaning the disk before writing to it (and this also means less clean up operations) as well as it has a better way of provisioning new areas for use on the disk (minimising the amount of writes of files).
      3. Finally over-provisioning can be used to extend the life of the SDD as it can both better optimise the clean up operations and be used to replace SSD space which is near its end of life (the dead pool).
      4. Over provisioning 20% of the SDD has been measure to give a 250% improvement on write speeds of a full disk and overprovision of 40% has been measured up to 380% improvement.
      5. Also Intel official white papers indicate that the over provisioning by 20% increases the endurance of a 160GB SSD by 400% so the writeable capacity of the SSD changes from 15TB to 61TB. @ 40% overprovisioning you get an additional 130% endurance and this increase is despite the fact we reduce the actual capacity available to the end-user.
      6. But please note that the size of the write blocks is a big factor in determining the improvement and the measures above were based on 4k blocks.
  7. And after all this is done its time to make sure your Windows settings are correct
    • Turn off hibernation mode unless you really need it
    • Fix you virtual memory settings
    • Index Service and Searching is a debateable point and I usually turn this off when running VHDs on my SSDs
    • Deactivate Prefecth/Superfetch
    • Ensure Write cache buffer is enabled
    • Active Write-cache buffer flashing
    • Use maximum power setting for a system which runs VHDs
    • System Restore is a space hog, use it wisely
  8. New into the whole arena is RAPID Mode from Samsung. Available on 840 EVO and Pro disks it uses memory to significantly speed up read and write back with some amazing speed improvements of 100% on read and 50% on writes
    • It can only be configured for a single disk in your system by using 1GB of your RAM, but it does introduce a significant risk if you do not have an uninterrupted power supply unit (UPS) as you run the risk of a power down leaving your system in an unpredictable state.
    • I highly recommend leaving this setting turned off unless you can prove it has a benefit to your system as this feature only has specific measureable benefits but at the same time it can cause slow downs in other areas. See the "techereport" posted below were the Samsung 840 Evo disk actually drops in rating when Rapid Mode is turned on.
 
 
   
Sources of material
  

Sunday 11 May 2014

AX 2012 R3 Licencing for each Role, and the journey to creating it




For months now I keep thinking about creating a way to keep the AX 2012 security spreadsheets up to date, lost countless hours of sleep over it (well I lost a couple of hours over the last couple of days trying to solve this )

Why I hear you ask, well firstly because the licensing spreadsheets on the internet are copies from AX2012 RTM (pre-R2) and are out of date. Secondly because as soon as you try to create your own security the lists gets even more out of date. Many a client have asked me about it and it is handy to be able to see how each role is put together and what impacts the license.

So after days of hard SQL slog, 100 lines of extreme TSQL, some X++ code and avoiding the Eurovision contest (Hooray for bearded folk) you can now access the up to date, R3 compliant security/licensing spreadsheet.
 

Here is a link to an R3 up to date License/Security spreadsheet. Please take it as a rough report, showing you the Roles by number of license entry points in both a high level view and by duties/privileges http://1drv.ms/1mQ35MR. Each entry point is analysed against its license. “None” indicates that only a server license is required.

Also a full analysis spreadsheet can be downloaded with full entry point disclosure e.g. Menu items http://1drv.ms/1mQ2ZF4

http://1drv.ms/1mQ35MR
Shared as an Excel Online spreadsheet in Onedrive.

The methods I used to achieve this results is worth noting, just in case you want to do this yourselves. To achieve this report I needed four things
  • The Entry points for each privilege and the appropriate License
  • The complete hierarchy of  all AX security from Roles, Sub-Roles, Duties and Privileges
  • The labels for all the AX objects
  • A way to combine all the above in a meaningful manner
So the first thing to note is that AX 2012 has a class which calculates the licenses per user, but only needs to do this by entry point and role. The class even has a helpful way to set up the batch job. I am of course referring to
SysUserLicenseMiner::createBatchJob();

 If you look through this class you will find two temporary tables which actually get truncated when the batch job runs. The table I need is the one concerned with licenses

SysUserLicenseMetadataTmp

I simply changed this from a TempDB table to a Regular table, as it is very useful to retain and its is not that big. 

So Entry points at hand I looked at the System Tables and found the Security Tables I needed to work out the hierarchy, better still there are a number of views based on these tables which helped me understand their purpose. Using SQL Management Studio i found out the following tables in the AX_Model database (where AX is replaced by your AX Database name)

  • ModelSecurityRole table holds all the key fields for all security artifacts as well as label reference Ids. Please note though this may have duplicate entries to accommodate layers. Hence many views group the records by RoleHandle (the security artifact unique ID) and use Max(RoleLayer) to find the highest layer for the artifact
  • ModelSecuritySubRole table contains all the relationships between artifacts e.g. how Duties related to roles
  • ModelSecurityPermission is the link between privileges and entry points, but you will need ModelSecurityCommon to go from a Named Entry Point to an ObjectHandle. Please note, entry points may require both an object and a child name e.g. server class and method name.
  • ElementTypes is a handy table to lookup types of objects and join this for more meaningful description. You will do well to look up other values in the System enums
  • ModelElementLabel is the language specific labels you will need to make things more meaningful to users
So now I have all the data, I had to work out the best way to decipher the info and construct a report. Given my experience in SQL I knew this would be a lot faster and more portable if I used TSQL to do this. TSQL is slower to produce than writing X++ code but it avoids any major system changes and is 100s of times faster from experience, well at least good SQL is faster :)
  • I used CTE (Common table expressions) to work through the recursive parts, but I ultimately used it to break the whole problem into unique Roles, unique & cascading sub-roles, unique & cascading duties, unique & cascading privileges and entry points
  • I decided that I would adjust the cascading structure and make it more consistent to enhance  readability of complex scenarios and simplify the TSQL, so it should have exactly five levels as follows
    • Role 1
      • Role 1 acting as a sub-role
        • Duties
          • Privileges
            • Entry Points
        • Add Duty to group Role based Privileges 
          • Privileges from Role
            • Entry Points
      • Sub-Role1
        • Duties etc
      • Flattened Sub-Role 1.1 (from Sub-Role 1)
        • Duties Etc
    • Roles 2 etc
  •  My final joins became easy due to the re-structure
    • Select Flattened Roles, Sub-Roles, Duties, Privileges and Entry Point
  • Finally I used a Pivot to change the license types into columns to simplify the report for users
And  the report was completed in such a way as to be ultra portable and flattened into an Excel tables. I was so glad I did that when I spotted the server only had Excel 2010 without Power Pivot, especially as I can hear you all complain about the flattened structure and duplication of data I had to use.

I am sure my next version will be a cascading table, separate label table and an entry point fact table for the licenses, but for now this will more than suffice.

I can run the routine any time I like, especially after any security changes and it only takes 30 seconds to populate the report. The entry point batch process runs once a night but I can also run this manually, and this takes 10 minutes to run.

I am not enclosing the TSQL as this is way to long for me to add a full explanation, and I want to change it to accommodate parameters and selection of db names, so for now the extracts will have to suffice, unless you try to contact me and allow me some time to explain it to you personally.

Enjoy the licensing spreadsheet, I know I had fun creating it.