New Posts New Posts RSS Feed: Performance Question
  FAQ FAQ  Forum Search   Calendar   Register Register  Login Login

Performance Question

 Post Reply Post Reply Page  12>
Author
skingaby View Drop Down
DevForce MVP
DevForce MVP
Avatar

Joined: 23-Apr-2008
Location: United States
Posts: 146
Post Options Post Options   Quote skingaby Quote  Post ReplyReply Direct Link To This Post Topic: Performance Question
    Posted: 28-Sep-2009 at 4:21pm
I've set up an AsyncParallelTask that runs 18 different entity queries to get all of the entities necessary for our grid form to be populated and editable. The main entity has a ton of foreign keys, types, statuses, children, etc, so there are a lot of queries to run.   We tried a giant entity query that used .Include() to pull in all the foreign keys but that query took FOREVER to run. So, I split it up and am running each query independently as an AsyncQuery task in an AsyncParallelTask. Most of the queries are short tables of 3-5 rows and a handful of columns, with a couple of them having 200-300 rows and 20 or so columns. None are large tables with thousands of rows. None have BLOB data in them. The database is Oracle, using the DevArt dotConnect drivers. I am using pre-compiled views in the ModelEF project.
The entire set takes just under 1 minute to run.
First, I broke the 18 queries down and ran each one separately. I.e. One query task for each run.
Then I added back each query one at a time to see what effect each additional query has on the total time. Times are in milliseconds. The logging is all client side and is displayed in a client-side log window as described elsewhere in this forum. The timer starts immediately prior to creating the AsyncParallelTask as in this snippet:

private void InitializeCache()
{
    Logging.TimerStart();
    task = AsyncParallelTask.Create();
    ...
    task.AddAsyncQuery(queryName, x => query, querySpecificCallback);
    ...
    task.Execute(InitializationCallback);
}
private void InitializationCallback(AsyncParallelTaskCompletedArgs args)
{
    Logging.TimerStop();
}

Timings:
1            - 9038.7200
2            - 8805.6945
3            - 8977.1328
4            - 9413.8232
5            - 9569.6812
6            - 9008.5924
7            - 9928.1546
8            - 9382.6516
9            - 9304.7823
10           - 9522.9849
11           - 9819.1170
12           - 9024.2361
13           - 9694.4298
14           - 9351.5400
15           - 10099.7280
16           - 9741.2500
17           - 10582.8940
18           - 10333.8556

1            - 9985.7012, 9810.7346
1+2          - 10998.5140
1+2+3        - 13736.2736
1 to 4       - 15233.2812
1-5          - 19135.6341
1-6          - 20859.5651
1-7          - 24581.1580
1-8          - 26080.3900
1-9          - 30593.7030
1-10        - 32208.2180
1-11        - 35689.7266
1-12        - 40383.2564, 36399.0270, 38258.0331,
1-13        - 41418.6368
1-14        - 45418.3866
1-15        - 47433.8568
1-16        - 48622.9544
1-17        - 55391.4640
1-18        - 58385.8935, 56948.4435

So, my question is:
What is each query doing that is taking 10 seconds? What are the queries doing that increment the total time by 3-4 seconds? Is there anything I can do to optimize this code / these queries / the EF loading times / the Entity construction time? Etc. Do you have any tips about enabling/disabling things at run time that can speed it up, for example, disabling the PropertyInterceptors on classes until AFTER the Async load has happened?
Thanks,
Simon
Back to Top
kimj View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 09-May-2007
Posts: 1391
Post Options Post Options   Quote kimj Quote  Post ReplyReply Direct Link To This Post Posted: 29-Sep-2009 at 6:32pm
I don't have an immediate answer for you, but I do have a few comments and more questions.
 
First, what does an async query do that would take 10 seconds?   Good question.  An async query has 3 "phases":  1) where it checks if connected and logged in and whether the query can be satisfied from cache; 2) the async phase, where the request is sent to the EntityServer on a separate thread; and 3) the merge of the returned data into the EntityManager cache, done back on the calling thread.  Various events are fired along the way also.  Why this whole process would take 10 seconds for very little data I don't know, since we haven't seen this issue in our tests.  Even launching Prism Explorer directly from our website, you generally see queries completing within a second or so. 
 
I would try to break this down a bit -- first see what the generated SQL looks like; next take that SQL and run it standalone within a query window (SQL*Plus or whatever) to see how long the database takes to execute it; next use Fiddler (or the network tool of your choice) to see what the request and response times are to the EntityServer.   If all of this looks reasonably good, then I'd focus on what's happening as the data is merged in the EM cache.  You will see the EntityChanging and EntityChanged events fired during the merge, and you'll see getter actions called if the data is bound to a UI control.  It might be worthwhile to retrieve the data without having bound it to anything to get a better idea of how long the merge into cache is taking.
 
Back to Top
skingaby View Drop Down
DevForce MVP
DevForce MVP
Avatar

Joined: 23-Apr-2008
Location: United States
Posts: 146
Post Options Post Options   Quote skingaby Quote  Post ReplyReply Direct Link To This Post Posted: 30-Sep-2009 at 7:44am
The generated SQL is quite normal looking, given how ugly some EF SQL can be, and the SQL response times running those queries are near instant.
I am going to add a counter to my logging so I can count how many times some things fire. Maybe I have out of control processes happening when the EntityManager assembles the items. I'm also going to look at what is happening if I run those same queries asynchronously and synchronously in the server-side DomainModel (from a unit test project) and what is happening if I run those same queries in the ServerModelEF project too. Thanks for the tips. I'll get back to you.
Back to Top
skingaby View Drop Down
DevForce MVP
DevForce MVP
Avatar

Joined: 23-Apr-2008
Location: United States
Posts: 146
Post Options Post Options   Quote skingaby Quote  Post ReplyReply Direct Link To This Post Posted: 01-Oct-2009 at 1:37pm
I tried a few things today:

1) I moved the queries into a client side Silverlight Unit Test so I could run them without any binding / UI issues. Completely removing XAML and the UI from the stack did NOT improve performance at all. The queries still take 60s to run. This also took all of the callbacks and post query processing off the board too. Here's the test code:

[TestMethod]
[Asynchronous]
public void InitializationQueries()
{
    var em = LocalEntityManager.DefaultManager;
    var task = AsyncParallelTask.Create();
    task.AddAsyncQuery("BusAssociates", x => em.BusAssociates, null);
    ...
    task.AddAsyncQuery("Zones", x => em.Zones, null);
    EnqueueCallback(() => Logging.TimerStart());
    EnqueueCallback(() => task.Execute(InitCompleted));
}
public void InitCompleted(AsyncParallelTaskCompletedArgs args) {
    var cacheInitializationElapsedTime = Math.Round(Logging.TimerStop()/1000,2);
    EnqueueTestComplete();
    double target = 15; //seconds
    Assert.IsTrue(cacheInitializationElapsedTime < target, String.Format("Query Initialization took {0}s, it should be less than {1}s", cacheInitializationElapsedTime, target));
}


2) I ran a unit test against the ServerModelEF project, thereby excluding all client-server, Ideablade and Silverlight code. The 18 queries run in less than 1 second.
The test looks like this:

[TestMethod]
public void InitializationQueries()
{
    ServerModelEF.ModelEntities em = new ModelEntities();
    var timerStart = DateTime.Now;
    var items01 = from item in em.BusAssociates select item;
    Assert.IsTrue(items01.Count() > 0);
    ...
    var items19 = from item in em.Zones select item;
    Assert.IsTrue(items19.Count() > 0);
    var elapsedTime = Math.Round(DateTime.Now.Subtract(timerStart).TotalMilliseconds / 1000, 2);
    double target = 1; //seconds
    Assert.IsTrue(elapsedTime < target, String.Format("Query Initialization took {0}s, it should be less than {1}s", elapsedTime, target));
}


3) I ran the same test in the DomainModel project, this adds the Ideablade EntityManager and Ideablade layer, but is still server side and still excludes the silverlight stuff. Same thing, all the queries run in a total of less than 1 second. I changed the test to use the DomainModelEntityManager, like this:

[TestMethod]
public void InitializationQueries()
{
    var em = new DomainModelEntityManager();
    em.InitializeEntityManager(); //logs in the EM
    var timerStart = DateTime.Now;
    ...
}


In conclusion, at this point I have established that the client-server / silverlight / async version of the queries takes 60s, but the server only / sync version of the queries takes only 1s.
Any thoughts on where I should go next? What is happening when an entity is Fetched? Do any property interceptors, validators, events, lazy loaders, PropertyChanged, or such fire? Where is the extra time coming from?

Edited by skingaby - 01-Oct-2009 at 1:38pm
Back to Top
kimj View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 09-May-2007
Posts: 1391
Post Options Post Options   Quote kimj Quote  Post ReplyReply Direct Link To This Post Posted: 01-Oct-2009 at 2:08pm
In test 3, did you also use the AsyncParallelTask?
 
You will have some performance hit in Silverlight because the data has to be sent across the wire, but that hit shouldn't be as big as you're seeing.  If you're not using a ServiceReferences.ClientConfig the default binding uses binary encoding.  (Beta versions of DevForce Silverlight used text encoding.)  If you are using a ClientConfig, what do the bindings look like?  I'd really recommend using Fiddler to inspect the messages and request/response times.  (The message sizes will probably be a bit large -- we've reduced some bloat in the coming release, and will be adding compression in a following release.)
 
Out of curiosity, if you run a single async query in Silverlight test, w/o using the AsyncParallelTask, how long does that take?
 
I think we already covered this before, but make sure the EntityManager used for the queries is both connected and logged in prior to executing the task.  If it's not, you'll see lots of superfluous connect/login calls to the server.
 
Async fetch logic, both pre- and post-, and the AsyncParallelTask, is the same in Silverlight as in non-Silverlight; so there is no difference in events fired or anything else.  Property interceptors and validators are not fired during the data load, nor is PropertyChanged.  The EntityChanging and EntityChanged events do fire.
 
Edited to add  -
The parallel task always returns the results from each async query back to the calling thread, so potentially you could see the async portion of all the queries complete, but the final post-processing involving the merge into the EntityManager cache all has to be done synchronously (because the EM is not threadsafe), so this could back up.  (I haven't actually seen this is testing, but I believe it's possible.)


Edited by kimj - 01-Oct-2009 at 2:15pm
Back to Top
skingaby View Drop Down
DevForce MVP
DevForce MVP
Avatar

Joined: 23-Apr-2008
Location: United States
Posts: 146
Post Options Post Options   Quote skingaby Quote  Post ReplyReply Direct Link To This Post Posted: 02-Oct-2009 at 6:37am
Sorry, I should have left more of the code in test 3. It used the exact code found in test 2. No AsyncParallelTask:

[TestMethod]
public void InitializationQueries()
{
    var em = new DomainModelEntityManager();
    em.InitializeEntityManager();
    var timerStart = DateTime.Now;
    var items01 = from item in em.BusAssociates select item;
    Assert.IsTrue(items01.Count() > 0);
    ...
    var items19 = from item in em.Zones select item;
    Assert.IsTrue(items19.Count() > 0);
    var elapsedTime = Math.Round(DateTime.Now.Subtract(timerStart).TotalMilliseconds / 1000, 2);
    double target = 1; //seconds
    Assert.IsTrue(elapsedTime < target, String.Format("Query Initialization took {0}s, it should be less than {1}s", elapsedTime, target));
}


We are not using a ServiceReferences.ClientConfig so the binding configuration is the same now as when we created the projects with the 5.2.1 release when Silverlight 3 was RTW. (Or, at least I think they are.)

Yes, the EM is logged in and connected. The InitializeEntityManager method in my DomainModelEntityManager partial takes care of that since we use the anonymous login because we're on Oracle.

I will run these other scenarios this morning: A) DomainModel Test on server using AsyncParallelTask; and B) ClientModel Silverlight Test on client NOT using AsyncParallelTask. I'm going to see if we have added anything to EntityChanging or EntityChanged (I don't think so, but I'm going to double check).

I was trying to figure out how to use Fiddler in my dev environment. The Cross Domain policy error happens because Fiddler changes the URL. Also, the VS2008 WebDev Server is running at http://localhost:9009 which Fiddler can't intercept because IE doesn't forward localhost to the proxy. Do you have a link that has good instructions for that? Or should I set up the Cross Domain policy? Thanks.
Back to Top
skingaby View Drop Down
DevForce MVP
DevForce MVP
Avatar

Joined: 23-Apr-2008
Location: United States
Posts: 146
Post Options Post Options   Quote skingaby Quote  Post ReplyReply Direct Link To This Post Posted: 02-Oct-2009 at 6:55am
This is interesting. Using the AsyncParallelTask in the DomainModel project (Server side only) sped up the load from 1.04s to 0.84s. Nice. Here's the test:

[TestMethod]
public void InitializationQueriesAsync()
{
    var em = new DomainModelEntityManager();
    em.InitializeEntityManager();
    var timerStart = DateTime.Now;
    var task = AsyncParallelTask.Create();
    task.AddAsyncQuery("BusAssociates", x => em.BusAssociates, null);
    ...
    task.AddAsyncQuery("Zones", x => em.Zones, null);

    //See blog: http://jasondotnet.spaces.live.com/blog/cns!BD40DBF53845E64F!170.entry
    //for how the AutoResetEvent is used
    this._TestTrigger = new AutoResetEvent(false);
    task.Execute(InitCompleted);
    this._TestTrigger.WaitOne();

    var elapsedTime = Math.Round(DateTime.Now.Subtract(timerStart).TotalMilliseconds / 1000, 2);
    double target = 1; //seconds
    Assert.IsTrue(elapsedTime < target, String.Format("Query Initialization took {0}s, it should be less than {1}s", elapsedTime, target));
}

AutoResetEvent _TestTrigger;

public void InitCompleted(AsyncParallelTaskCompletedArgs args) {
    this._TestTrigger.Set();
}


Edited by skingaby - 02-Oct-2009 at 7:02am
Back to Top
kimj View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 09-May-2007
Posts: 1391
Post Options Post Options   Quote kimj Quote  Post ReplyReply Direct Link To This Post Posted: 02-Oct-2009 at 9:10am
Something I just noticed which isn't really pertinent to the problem but which may skew the timings, is that in your tests not using the AsyncParallelTask you're not actually retrieving any data, you're just retrieving a count --
 
    var items01 = from item in em.BusAssociates select item;
    Assert.IsTrue(items01.Count() > 0);
 
.. this selects only the number of items, it doesn't retrieve the items and then do a count.  DevForce handles the retrieval of a primitive much differently than the retrieval of entities which need to be merged into cache.  When going over the wire too there's of course a big difference in message size.
 
I didn't know you were working with the development web server, in that case, I use Firefox and Firebug (which may require Fiddler too I'm not sure, but integrates really well with it).  Ward blogged about how to set up Firefox and Fiddler - http://neverindoubtnet.blogspot.com/2009_08_01_archive.html.
 
 
Back to Top
skingaby View Drop Down
DevForce MVP
DevForce MVP
Avatar

Joined: 23-Apr-2008
Location: United States
Posts: 146
Post Options Post Options   Quote skingaby Quote  Post ReplyReply Direct Link To This Post Posted: 02-Oct-2009 at 10:05am
I did not realize that. I have changed it to do this:

[TestMethod]
public void InitializationQueries()
{
    ServerModelEF.ModelEntities em = new ModelEntities();
    var timerStart = DateTime.Now;
    var items01 = from item in em.BusAssociates select item;
    ScanAll(items01);
    ...
    var items19 = from item in em.Zones select item;
    ScanAll(items19);
    var elapsedTime = Math.Round(DateTime.Now.Subtract(timerStart).TotalMilliseconds / 1000, 2);
    double target = 1; //seconds
    Assert.IsTrue(elapsedTime < target, String.Format("Query Initialization took {0}s, it should be less than {1}s", elapsedTime, target));
}

private void ScanAll(IQueryable qry)
{
    foreach (var item in qry)
    {
        //do nothing
    }
}


That should retrieve all the items, shouldn't it?
That brought the timing of the ServerModelEF test up from 1s to 2.3s.
Back to Top
kimj View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 09-May-2007
Posts: 1391
Post Options Post Options   Quote kimj Quote  Post ReplyReply Direct Link To This Post Posted: 02-Oct-2009 at 10:23am
A Count() clause causes immediate execution of the query -- it's a LINQ thing more than a DevForce thing.  To retrieve data you can use ToList() (or ToArray()) to force execution, or enumerate the query with a for/each.   EF also lets you call Execute on the query, and DevForce has all its variations on Execute or ExecuteQuery.  I always recommend that developers read up on deferred vs. immediate execution in LINQ, since it's not really intuitive and the cause of lots of problems. 
Back to Top
skingaby View Drop Down
DevForce MVP
DevForce MVP
Avatar

Joined: 23-Apr-2008
Location: United States
Posts: 146
Post Options Post Options   Quote skingaby Quote  Post ReplyReply Direct Link To This Post Posted: 02-Oct-2009 at 11:26am
Here's the Daisy Chain version of the test running client side in a Silverlight Unit Test. This one calls em.ExecuteQueryAsync on each of the queries and then calls the next query in that ones callback. Total time is 30 seconds.

[TestClass]
public class DealQueriesSerial : SilverlightTest
{
    
    DomainModelEntityManager _em = new DomainModelEntityManager();

    [TestMethod]
    [Asynchronous]
    public void TestAQueryPerf()
    {
        _em.InitializeEntityManagerCompleted += new EventHandler(TestAQueryPerf_em_InitializeEntityManagerCompleted);
        EnqueueCallback(() => _em.InitializeEntityManager());
    }

    private void TestAQueryPerf_em_InitializeEntityManagerCompleted(object sender, EventArgs e)
    {
        EnqueueCallback(() => Logging.TimerStart());
        EnqueueCallback(() => _em.ExecuteQueryAsync(_em.BusAssociates, TestAQueryPerf_QueryCallback01, null));
    }

    private void TestAQueryPerf_QueryCallback01(EntityFetchedEventArgs args)
    {
        EnqueueCallback(() => _em.ExecuteQueryAsync(_em.Contacts, TestAQueryPerf_QueryCallback02, null));
    }

    ...

    private void TestAQueryPerf_QueryCallback18(EntityFetchedEventArgs args)
    {
        EnqueueTestComplete();
        var elapsedTime = Math.Round(Logging.TimerStop() / 1000, 2);
       
        double target = 1; //seconds
        Assert.IsTrue(elapsedTime < target, String.Format("Query Initialization took {0}s, it should be less than {1}s", elapsedTime, target));
    }
}


I also tried a parallel run by just calling ExecuteAsync on all the queries in one method. This killed the performance and takes over 160 seconds to run. Yeuch.

[TestClass]
public class DealQueriesParallel : SilverlightTest
{
    
    DomainModelEntityManager _em = new DomainModelEntityManager();

    [TestMethod]
    [Asynchronous]
    public void TestAQueryPerf()
    {
        _em.InitializeEntityManagerCompleted += new EventHandler(TestAQueryPerf_em_InitializeEntityManagerCompleted);
        EnqueueCallback(() => _em.InitializeEntityManager());
    }

    private void TestAQueryPerf_em_InitializeEntityManagerCompleted(object sender, EventArgs e)
    {
        EnqueueCallback(() => Logging.TimerStart());
        EnqueueCallback(() => _em.ExecuteQueryAsync(_em.BusAssociates, TestAQueryPerf_QueryCallback, null));
        ...
        EnqueueCallback(() => _em.ExecuteQueryAsync(_em.Zones, TestAQueryPerf_QueryCallback, null));
    }

    private int TestAQueryPerf_QueryCallback_Counter = 0;
    private void TestAQueryPerf_QueryCallback(EntityFetchedEventArgs args)
    {
        TestAQueryPerf_QueryCallback_Counter++;
        if (TestAQueryPerf_QueryCallback_Counter == 19)
        {
            EnqueueTestComplete();
            var elapsedTime = Math.Round(Logging.TimerStop() / 1000, 2);

            double target = 1; //seconds
            Assert.IsTrue(elapsedTime < target, String.Format("Query Initialization took {0}s, it should be less than {1}s", elapsedTime, target));
        }
    }
}
Back to Top
kimj View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 09-May-2007
Posts: 1391
Post Options Post Options   Quote kimj Quote  Post ReplyReply Direct Link To This Post Posted: 02-Oct-2009 at 11:54am
Okay.  So where are we now?  I think you've got these timings ... (?)
 
AsyncParallelTask (with 18 queries) in SL        = 60 seconds
daisy chained 18 queries in SL                         = 30 seconds
18 async queries in SL                                     = 160 seconds
AsyncParallelTask (with 18 queries) on server = under 1 second
 
I also want to test the AsyncParallelTask with a BOS in non-SL to get an idea of what the transmission hit is between a BOS and no-BOS run.  I can do this later today in our test suite.  Right now I think it's still unclear what is transmission-related overhead, what is SL-related, and what is AsyncParallelTask-related.
Back to Top
skingaby View Drop Down
DevForce MVP
DevForce MVP
Avatar

Joined: 23-Apr-2008
Location: United States
Posts: 146
Post Options Post Options   Quote skingaby Quote  Post ReplyReply Direct Link To This Post Posted: 02-Oct-2009 at 12:03pm
Right.
That's what I think too.

As an aside, what am I looking for in Fiddler? I have the Firefox/Fiddler solution working. Thanks for the link. There's a ton of activity when I run the tests and there are some big chunks of message coming back, but all of the items have a TotalElapsedTime that is a fraction of a second.
Back to Top
kimj View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 09-May-2007
Posts: 1391
Post Options Post Options   Quote kimj Quote  Post ReplyReply Direct Link To This Post Posted: 05-Oct-2009 at 2:08pm
Just an update to let you know we're still looking at this.  The problem is Silverlight-specific, and likely due to the number of threads started or to possible limitations with WCF in Silverlight.  We'll keep you updated. 
 
As for a workaround for you right now, I don't know what to tell you.  Presumably the AsyncSerialTask will be faster, but at about 30 seconds probably still too slow.  You might be able to use .Include with some of your queries.  I know you'd tried a giant .Include query with poor performance, but maybe "judicious" use of .Include which keeps the SQL queries from getting too ugly might be a compromise.
 
Back to Top
skingaby View Drop Down
DevForce MVP
DevForce MVP
Avatar

Joined: 23-Apr-2008
Location: United States
Posts: 146
Post Options Post Options   Quote skingaby Quote  Post ReplyReply Direct Link To This Post Posted: 06-Oct-2009 at 6:28am
Thank you for looking in to this. Performance is DEFINITELY our highest priority. The server-side performance is quite acceptable. One of the configurations runs all the queries in a total of less than 4 seconds. I think there's a configuration that actually loads them in less than 1 second, but I'm not sure off the top of my head. Anyway, when that 4 seconds becomes 30 - 160 seconds to load the same data to the client, that's where we have to find a way to improve the performance significantly.
Thanks, Simon.
Back to Top
kimj View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 09-May-2007
Posts: 1391
Post Options Post Options   Quote kimj Quote  Post ReplyReply Direct Link To This Post Posted: 06-Oct-2009 at 8:56am
I understand.
 
Meanwhile, I keep trying to think of workarounds ... the EntityCacheState will serialize to the SL client correctly in v5.2.3 coming Oct. 15, so grabbing all this data via an invoked server method which does the AsyncParallelTask and returns an EntityCacheState could be another option.  PM or email me if this is something you'd want to try now.
Back to Top
 Post Reply Post Reply Page  12>

Forum Jump Forum Permissions View Drop Down