Ayende @ Rahien

Refunds available at head office

NuGet Perf, The Final Part – Load Testing – Setup

So, after talking so long about the perf issues, here is the final part of this series. In which we actually take this for a spin using Load Testing.

I built a Web API application to serve as the test bed. It has a RavenController, which looks like this:

public class RavenController : ApiController
{
    private static IDocumentStore documentStore;

    public static IDocumentStore DocumentStore
    {
        get
        {
            if (documentStore == null)
            {
                lock (typeof (RavenController))
                {
                    if (documentStore != null)
                        return documentStore;
                    documentStore = new DocumentStore
                        {
                            Url = "http://localhost:8080",
                            DefaultDatabase = "Nuget"
                        }.Initialize();
                    IndexCreation.CreateIndexes(typeof (Packages_Search).Assembly, documentStore);
                }
            }
            return documentStore;
        }
    }

    public IDocumentSession DocumentSession { get; set; }

    public override async Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)
    {
        using (DocumentSession = DocumentStore.OpenSession())
        {
            HttpResponseMessage result = await base.ExecuteAsync(controllerContext, cancellationToken);
            DocumentSession.SaveChanges();
            return result;
        }
    }
}

And now we have the following controllers:

public class PackagesController : RavenController
{
    public IEnumerable<Packages_Search.ReduceResult> Get(int page = 0)
    {
        return DocumentSession.Query<Packages_Search.ReduceResult, Packages_Search>()
            .Where(x=>x.IsPrerelease == false)
            .OrderByDescending(x=>x.DownloadCount)
                .ThenBy(x=>x.Created)
            .Skip(page*30)
            .Take(30)
            .ToList();
    }
}

public class SearchController : RavenController
{
    public IEnumerable<Packages_Search.ReduceResult> Get(string q, int page = 0)
    {
        return DocumentSession.Query<Packages_Search.ReduceResult, Packages_Search>()
            .Search(x => x.Query, q)
            .Where(x => x.IsPrerelease == false)
            .OrderByDescending(x => x.DownloadCount)
                .ThenBy(x => x.Created)
            .Skip(page * 30)
            .Take(30)
            .ToList();
    }
}

And, just for completeness sake, the Packages_Search index looks like this:

public class Packages_Search : AbstractIndexCreationTask<Package, Packages_Search.ReduceResult>
{
    public class ReduceResult
    {
        public DateTime Created { get; set; }
        public int DownloadCount { get; set; }
        public string PackageId { get; set; }
        public bool IsPrerelease { get; set; }
        public object[] Query { get; set; }
    }

    public Packages_Search()
    {
        Map = packages => from p in packages
                          select new
                              {
                                  p.Created, 
                                  DownloadCount = p.VersionDownloadCount, 
                                  p.PackageId, 
                                  p.IsPrerelease,
                                  Query = new object[] { p.Tags, p.Title, p.PackageId}
                              };
        Reduce = results =>
                 from result in results
                 group result by new {result.PackageId, result.IsPrerelease}
                 into g
                 select new
                         {
                             g.Key.PackageId,
                             g.Key.IsPrerelease,
                             DownloadCount = g.Sum(x => x.DownloadCount),
                             Created = g.Select(x => x.Created).OrderBy(x => x).First(),
                             Query = g.SelectMany(x=>x.Query).Distinct()
                         };

        Store(x=>x.Query, FieldStorage.No);
    }
}

That is enough setup, in the next post, I’ll discuss the actual structure of the load tests.

Comments

Simon Hughes
09/17/2012 11:38 AM by
Simon Hughes

Best not to use lock (typeof (RavenController)), as some other code could also lock on that type. Better to use a private lock field instead.

What about using Lazy instead?

Simon Hughes
09/17/2012 11:39 AM by
Simon Hughes

hmm, my less than and greater than were removed. Lazy < DocumentStore >

Adam Ralph
09/17/2012 11:55 AM by
Adam Ralph

+1 Simon Hughes... never lock on something which other peoples code can see.

Chris
09/17/2012 01:59 PM by
Chris

I'll echo the other guys about the lock(typeof(...)). Locking on a Type instance is quite dangerous as the same instance can be used across AppDomains. The documentation (http://msdn.microsoft.com/en-us/library/c5kehkcz(v=vs.80).aspx) says that it is only a problem if the Type is publicly accessible, but it seems to me that it could also be a problem if someone got the Type instance through reflection or returned polymorphically through an interface or something.

Here is an analysis of the problem: http://www.pcreview.co.uk/forums/dont-lock-type-objects-t1373491.html (this is from 2004, so it may not be completely accurate anymore).

Matt Johnson
09/17/2012 03:48 PM by
Matt Johnson

Ayende, I thought the general guidance was to initialize the documentstore during application startup in global.asax? The way you have it here, there will be a delay when the first user hits the controller.

Also, this creates a separate documentstore for each controller, which I also thought was bad form. Shouldn't there just be one per app?

Chanan Braunstein
09/17/2012 04:34 PM by
Chanan Braunstein

Ayende, What is the purpose of Store(x=>x.Query, FieldStorage.No);? What does it do if it is not storing the query field in the index? When should it be used, etc...

João P. Bragança
09/18/2012 04:13 AM by
João P. Bragança

What other code is actually going to lock on that type? this is not production code, it's for a PERF TEST. Good lord.

tatabánya gyerekangol
09/18/2012 10:55 AM by
tatabánya gyerekangol

@chanan braunstein: may it's not just about fields...but I'm courious too:)

Simon
09/18/2012 11:22 AM by
Simon

@Matt The document store is static, so there will only be one per app not one for each controller. The delay is effectively the same as in Application_Start, as that won't fire until the first request anyway (assuming not using the new "warm-up" app thingy added in ASP.NET 4, and that the first request will end up hitting a controller).

Matt Warren
09/18/2012 12:13 PM by
Matt Warren

@Chanan Braunstein, @tatabánya gyerekangol

For a MapReduce index the default is FieldStorage.Yes, so this is just an optimisation.

You can still query against a field when it has FieldStorage.No, but it means that you just can't read that value from the index. So in this case reading the contents of "Query" field is not needed, so not storing it saves space.

Matt Johnson
09/20/2012 09:15 PM by
Matt Johnson

@Simon - Thanks for pointing that out. I was thinking about the problem of static fields in GENERIC classes. You are right, there will only be one static documentstore instance here, so I guess it's ok.

Comments have been closed on this topic.