nopCommerce 1.7 cache not working?! Possible solution to 1.7 slowness?

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.
13 years ago
deval wrote:
Hi there,

I absolutley disagree using the cache per request. There might be some exclusions for data that is updated very frequently, that is alright, but not for stuff like searchresults or product details.

I'm too am a bit dissapointed that most of the stuff has moved to the NopRequestCache.
10 User hit one page wich generates 40 queries. => 400 rqueries in total for just 10 users.


It would be good to have this configurable. Have a common interface for NopRequestCache and NopStaticCache like INopCache object.
In methods like GetCategoryById() you would deal with the INopCache object. where you add, remove objects.

The underlying logic which cache type is to be returned would be done by a FactoryMethod, where you could place even more config options.
This shouldn't be that complicated and extensible.
As for exceptions.
We are talking here only about the front end, not the administration part.
When all objects are detached from the ObjectContext, I can't see how you would get any exceptions?
I changed all the search results to come from a single aggregated table, which results in turn are fully cached as well. There are over 20.000 products in there and I never had one exception and it is very fast. Unfortunately I have now even more queris as stuff moved from Static to Request. Hm

My thought,
Deval


Please vote Deval http://nopcommerce.codeplex.com/workitem/9445 - its the only way they will listen.
13 years ago
I think I may be rolling back to 1.6 or implementing a CDN edge against the platform to "wax over" the caching issues.

The queries also need better tuning.  The query that is lagging out on you is the productLoadAllPaged which I tuned in the following post:

http://www.fuzionagency.com/?tag=/NOP-Commerce

Maybe that's worth a look.  My query speed went from over 75 seconds  to less than 3

Cheers,

Jared Nielsen
www.FUZION.org
13 years ago
Hi Jared, I just read the page you pointed to. I'm not a SQL Guru (since I am a front end designer) but I read and understand your post in all detail and is a real eye opener on better query design. The post from 'first option mortage' is also a good read using the CTE of MS SQL for the picture blobs.

I hope the NC team can learn something from you.
13 years ago
Jared,

That's great input regarding the tuning of the existing stored procedure and is definitely a quick fix.

However, in the next release of nopCommerce I would like to see us move away from stored procedures completely (unless absolutely necessary).

I have already discussed how we can flex IQueryable for a better paging solution (see my post here - http://blogs.planetcloud.co.uk/mygreatdiscovery/post/Efficient-GridView-paging-with-IQueryable.aspx).

By taking advantage of deferred query execution and some refactoring we can certainly improve performance.

The first thing I would recommend is we get rid of this:


        public static List<Product> GetAllProducts(int categoryId,
            int manufacturerId, int productTagId, bool? featuredProducts,
            decimal? priceMin, decimal? priceMax,
            int relatedToProductId, string keywords, bool searchDescriptions, int pageSize,
            int pageIndex, List<int> filteredSpecs, int languageId,
            ProductSortingEnum orderBy, out int totalRecords) { .... yuck .... }


The problem with making a method like this so generic is that you just end up with many overloads. It also makes caching a nightmare (ref:Deval) - try coming up with a cache key for this one. As you can see currently, we are not even caching these result.

Instead we should refactor into more specific methods that can return a paged list (see the link above for a pagedlist implementation).

For example:


        public static PagedList<Product> GetProductsInCategory<TKey>(int categoryId,
            decimal? priceMin, decimal? priceMax, int pageSize,
            int pageIndex, Expression<Func<Product, TKey>> orderBy) {

            var db = ObjectContextHelper.CurrentObjectContext;

            var query = from p in db.Products
                        from pc in p.NpProductCategories
                        where pc.CategoryId == categoryId
                        select p;

            if (priceMin.HasValue && priceMax.HasValue)
            {
                query = from p in query
                        from pv in p.NpProductVariants
                        where pv.Price <= priceMax.Value &&
                            pv.Price >= priceMin.Value
                        select p;
            }

            return new PagedList<Product>(query.OrderBy(orderBy), pageIndex, pageSize);
        }


Usage:


            var productCollection = ProductManager.GetProductsInCategory<string>(category.CategoryId,
                minPriceConverted, maxPriceConverted, pageSize, this.CurrentPageIndex, x => x.Name);


Okay so we can make this method better, but you get the idea.

Next, recommendation, get a copy of EF Prof so you can see exactly what SQL is generated. When it comes to ORM, tools like this are just essential (we use NH Prof extensively for NHibernate).

A quick test with the original stored procedure showed a query execution time of 24ms. Swapping this out with the PagedList implementation brought it down to <1ms - the SQL generated:


SELECT   TOP ( 10 ) [Project1].[ProductId]            AS [ProductId],
                    [Project1].[Name]                 AS [Name],
                    [Project1].[ShortDescription]     AS [ShortDescription],
                    [Project1].[FullDescription]      AS [FullDescription],
                    [Project1].[AdminComment]         AS [AdminComment],
                    [Project1].[TemplateID]           AS [TemplateID],
                    [Project1].[ShowOnHomePage]       AS [ShowOnHomePage],
                    [Project1].[MetaKeywords]         AS [MetaKeywords],
                    [Project1].[MetaDescription]      AS [MetaDescription],
                    [Project1].[MetaTitle]            AS [MetaTitle],
                    [Project1].[SEName]               AS [SEName],
                    [Project1].[AllowCustomerReviews] AS [AllowCustomerReviews],
                    [Project1].[AllowCustomerRatings] AS [AllowCustomerRatings],
                    [Project1].[RatingSum]            AS [RatingSum],
                    [Project1].[TotalRatingVotes]     AS [TotalRatingVotes],
                    [Project1].[Published]            AS [Published],
                    [Project1].[Deleted]              AS [Deleted],
                    [Project1].[CreatedOn]            AS [CreatedOn],
                    [Project1].[UpdatedOn]            AS [UpdatedOn]
FROM     (SELECT [Project1].[ProductId]                 AS [ProductId],
                 [Project1].[Name]                      AS [Name],
                 [Project1].[ShortDescription]          AS [ShortDescription],
                 [Project1].[FullDescription]           AS [FullDescription],
                 [Project1].[AdminComment]              AS [AdminComment],
                 [Project1].[TemplateID]                AS [TemplateID],
                 [Project1].[ShowOnHomePage]            AS [ShowOnHomePage],
                 [Project1].[MetaKeywords]              AS [MetaKeywords],
                 [Project1].[MetaDescription]           AS [MetaDescription],
                 [Project1].[MetaTitle]                 AS [MetaTitle],
                 [Project1].[SEName]                    AS [SEName],
                 [Project1].[AllowCustomerReviews]      AS [AllowCustomerReviews],
                 [Project1].[AllowCustomerRatings]      AS [AllowCustomerRatings],
                 [Project1].[RatingSum]                 AS [RatingSum],
                 [Project1].[TotalRatingVotes]          AS [TotalRatingVotes],
                 [Project1].[Published]                 AS [Published],
                 [Project1].[Deleted]                   AS [Deleted],
                 [Project1].[CreatedOn]                 AS [CreatedOn],
                 [Project1].[UpdatedOn]                 AS [UpdatedOn],
                 row_number()
                   OVER(ORDER BY [Project1].[Name] ASC) AS [row_number]
          FROM   (SELECT [Extent1].[ProductId]            AS [ProductId],
                         [Extent1].[Name]                 AS [Name],
                         [Extent1].[ShortDescription]     AS [ShortDescription],
                         [Extent1].[FullDescription]      AS [FullDescription],
                         [Extent1].[AdminComment]         AS [AdminComment],
                         [Extent1].[TemplateID]           AS [TemplateID],
                         [Extent1].[ShowOnHomePage]       AS [ShowOnHomePage],
                         [Extent1].[MetaKeywords]         AS [MetaKeywords],
                         [Extent1].[MetaDescription]      AS [MetaDescription],
                         [Extent1].[MetaTitle]            AS [MetaTitle],
                         [Extent1].[SEName]               AS [SEName],
                         [Extent1].[AllowCustomerReviews] AS [AllowCustomerReviews],
                         [Extent1].[AllowCustomerRatings] AS [AllowCustomerRatings],
                         [Extent1].[RatingSum]            AS [RatingSum],
                         [Extent1].[TotalRatingVotes]     AS [TotalRatingVotes],
                         [Extent1].[Published]            AS [Published],
                         [Extent1].[Deleted]              AS [Deleted],
                         [Extent1].[CreatedOn]            AS [CreatedOn],
                         [Extent1].[UpdatedOn]            AS [UpdatedOn]
                  FROM   [dbo].[Nop_Product] AS [Extent1]
                         INNER JOIN [dbo].[Nop_Product_Category_Mapping] AS [Extent2]
                           ON [Extent1].[ProductId] = [Extent2].[ProductID]
                  WHERE  [Extent2].[CategoryID] = 29 /* @p__linq__0 */) AS [Project1]) AS [Project1]
WHERE    [Project1].[row_number] > 0
ORDER BY [Project1].[Name] ASC


As I said the main advantage of using a tool like EF Prof is that you can then see exactly what SQL is being generated so you can then further tune your object queries.

Deval,

Good suggestion. Shame really that everything is static. We would probably need to have a static ICache property on each Manager that news up the appropriate ICache implementation

e.g.


        public static ICache Cache
        {
            return new NopMemoryCache();
        }


Then we can just call

if (this.Cache.Contains(key))
var products = this.Cache.Get<IList<Product>>(key);

If I get time this week I will have a go at testing this out.
13 years ago
I'd like to test your new query too but am using 1.80. Has this one change since 1.70?
13 years ago
HI,

just as another idea,
public static List<Product> GetAllProducts(.....)
this method as stated has lots of parameters which makes it not suitable for caching, there would be just too many permutations.

But you can still cache the core results and then work with linq to objects. I did this with several implemantations and it works flawlessy.

This is what you could do in the GetAllProducts method
Step 1. Get all products from cache, no filters are aplied yet
Step 2. then you apply the where clause with linq

Here is an example of how I implemented it (merged some methods to make it clear what i mean). Don't get confused about the class names used. I added some but this logic can be applied to all appropriate Nop classes

function List<FrontSearchResult> GetProducts(methodParam1, .......)
{
            //  This is from CacheManager.GetFrontSearchResults()
            object o = EXSCache.Get("EXSFrontSearchResultCollection");
            if (o == null)
                FillCacheWithEXSFrontSearchResultCollection();
            o = EXSCache.Get("EXSFrontSearchResultCollection");
            //

            List<FrontSearchResult> sp = (List<FrontSearchResult>)o;
            var query = sp.AsQueryable();
            
            //here we have a queryable object so you can do whatever you want to do
            query = query.Where(result => result.PriceWithTax < methodParam1);
            query = query.Where(result => result.WhatEverAttribute.Contains(methodParam2));
            // and so on, this can be quite a long list

            // at the end you return
            return query.ToList(); // only here the where clauses are actually applied.
           // NO SQL calls are made, everything is done in memory
}



I personally implemented this for all front facing methods.
CacheManager takes care of how long to cache every list. it even listens to OrderPlaced etc. to update the cache in case you don't want to show products with quantity = 0 etc.

Hope this is an idea for some of you.

Cheers
13 years ago
retroviz,

I'm with you on the paging fix.

I have read your post before and I really want to try integrating that in.  I agree that the current proc is wearing like 10 different hats  (hydrate objects for search results, filtering by language, etc) so that would lend itself to multiple targeted methods.

I just got all excited about NOP commerce and was blindly upgrading without properly testing it and now I've got production sites that are crashing my servers so I had to do something quickly or my cataloging teams were going to start eating my children in revenge.

When the natives are restless, you have to find quick solutions.

I'm excited about the new tool.  I'll definitely be giving that a look see.

Don't eliminate CDN from the strategy.  I far prefer spending my time coding against a CDN cache than hyper-optimizing my code.  Way simpler and has many other intrinsic benefits.  I'll post a blog about that as well.  NOP has to have some modifications to survive a CDN implementation (recently viewed, and profile based pricing goes away for example).

Now I'm off to bandaid the product detail load times .... absolutely unusable in its current state
http://www.jacksonvilletires.com/products/15-nitto-neo-gen-all-season-ultra-high-performance-radial-tire-for-lowered-vehicles.aspx]http://www.jacksonvilletires.com/products/15-nitto-neo-gen-all-season-ultra-high-performance-radial-tire-for-lowered-vehicles.aspx

Cheers,

Jared Nielsen
13 years ago
ajhvdb,

If I get some time I'll take a whack at 1.8 but I've avoided it for now.  I'm lamenting moving to 1.7 so far until I get this all resolved.  Love the platform, just wish I had tested before I leaped on this last upgrade.

Jared Nielsen
FUZION Agency
13 years ago
Fuzion

The page you posted in really really slow.

The main problem you have, as you know, it that you have so many product variants for this product.
As a test, try to uncomment everyting in the  OnItemDataBound of the ProductVariantsInGrid.ascx, just to see if calls made in here have to be modified. Then step by step uncomment blocks of lines.

Also, do you serve your pictures from the db or filesystem?
DB is usually slower in lots of scenarios.

Let me know how it turned out.

Deval
13 years ago
deval wrote:
Fuzion

The page you posted in really really slow.

The main problem you have, as you know, it that you have so many product variants for this product.
As a test, try to uncomment everyting in the  OnItemDataBound of the ProductVariantsInGrid.ascx, just to see if calls made in here have to be modified. Then step by step uncomment blocks of lines.

Also, do you serve your pictures from the db or filesystem?
DB is usually slower in lots of scenarios.

Let me know how it turned out.

Deval


Images are cached in the file system even if using the DB.
This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.