Solved

Help with Delphi 2009 Performance when writing a large loop count

Posted on 2010-08-16
39
595 Views
Last Modified: 2013-11-23
I have an application developed in Delphi 2009 that is giving me some performance nightmares. The application reads data from an excel spreadsheet into a text file and then parses that text file to extract structured information and then writes that information to a business modelling application using COM.

Some of the files are pretty lengthy and can be 7,000 - 10,000 lines long. The analysis and parsing stage works very fast - the problem comes when we add the data to the business modelling application - the import just gets slower and slower until each update is taking over 15 seconds.

However, and this is really weird, if I leave the application running and click and hold the mousr button down with the cursor on any other window - the import speeds up to full speed again. Take your finger off the mouse button and my app slows down again.

Has anyone got any ideas what may be causing this...  
0
Comment
Question by:davidcase
  • 13
  • 10
  • 10
  • +1
39 Comments
 
LVL 25

Expert Comment

by:epasquier
Comment Utility
What is this COM application you are refering to ?

Can you give more specific performance, like the global time to import one specific large file, and the time to "import" that same file if you comment just the code writing the data ?

Also, which OS have you, and have you tried on different hardware, OS etc ... ?
0
 

Author Comment

by:davidcase
Comment Utility
Hi,
Taking you questions one at a time...

The CPM Server app is a Financial Modelling tool that allows users to model their financial business data through time. This is a stand-alone application but it does expose certain model creation capabilities by use of COM - Much like Word or Excel.

The application I have written in D2009 reads in large Excel files (directly - I do not use COM for this) and creates a blank model and writes thye data to the Financial Modelling application using its exposed COM interface.

I have timed the parsing of a 3000 line x 28 column spreadsheet, reading the appropriate data into a structured tree (so we can save to XML if we need to) and storing this tree ready to use to create the imported model. This takes a few seconds. Running on a liquid cooled 6 core machine, W7 64bit 8GB Ram and fast HDD's.

The import application then begins creating the structure of the financial model by creating the properly formatted tree stucture and writing the source data (floats, strings, dates etc) to the target model. As stated earlier I use the COM layer to do this as the file system of the modeller is extremely complex using old structured storage and some pretty weird dynamics.

After importing 500 lines of data the importer begins to slow down almost exponentially as each dataline takes longer and longer to import. By the time we are up to 3,000 lines imported it can take 4 seconds to import each line. Above 5,000 items we are in the region of 12 seconds per line import.

However, when I open any other application while the importer is running, Skype is a good example and was how I discovered this crazy behavior, the following happens.

I put both apps side by side and can see the imported data counter on the importer and the progress bar.

I then started to move the skype window and noticed that, after I had clicked the Skype title bar and held the left mouse button down to drag the Skype Window, the importer application speeded up by a very significant amount. We went from one update every 8 seconds to 10 per second.

The moment I let the mouse key return to the up position the importer immediately returned to the previous slow operation. Pressing and holding the left mouse button down again on "any" external application or even the background wallpaper caused the immediiate increase in speed detailed above.

If we can identify what causes this behavior I may be able to integrate this into my application somehow.

I have not tried on other hardware yet but will try to do this over the coming wekend.

I hope this gives a better picture of what is happening.

Thanks.
David.
0
 
LVL 36

Expert Comment

by:Geert Gruwez
Comment Utility
looks like a badly written import routine

this may be odd, but ...
> get a heavy book and position it on the mouse button
0
 
LVL 36

Expert Comment

by:Geert Gruwez
Comment Utility
maybe the app has some other actions happening  in the front end
if it looses focus, it doesn't do those actions any more.

you could look in the task manager/process explorer to see how many threads are running
in process explorer you could open the lower pane to see what the app is holding of files/handles
you can get process explorer from http://www.sysinternals.com

you could then look in process explorer what's happened with the app focused and when not focused





0
 
LVL 45

Expert Comment

by:aikimark
Comment Utility
either Excel import or COM output may be poorly written.  For instance, if the COM object instantiation takes place inside the loop, it will certainly cause performance problems.

Please post the code for both routines.  Be sure to obfuscate any user ID or password data in the code before posting it.
0
 
LVL 25

Expert Comment

by:epasquier
Comment Utility
I bet my money on the problem coming from the financial application internal handling of data (and system messages) causing the exponential time needed to perform its COM called functions. I couldn't tell much more without knowing the interface, and what it does.

I believe your best solution would be to call the support of this application and expose them with the problem. Maybe they could tell us some insider information about how it works, or identify by tracing some key operations what is eating time, and then we could help finding a solution.

You'd better check on another computer/OS if the problem is also there, that will help you convincing them that it is their application that have issues, not yours.

You can post your code if you wish/can - just to check that there is no obvious problems with it - but I really don't think we will find the solution there.
0
 
LVL 25

Expert Comment

by:epasquier
Comment Utility
I forgot to mention that I like Geert solution with a heavy book !
That is incredibly pragmatic, and probably the best 'solution quality' / 'time spent ' you could achieve. I know it sounds awful, but that is definitely a good alternative while the "real" solution is looked for.
0
 
LVL 25

Expert Comment

by:epasquier
Comment Utility
> For instance, if the COM object instantiation takes place inside the loop,
> it will certainly cause performance problems.
True, if you constantly create / free the object within your loop, and that object is persistent (otherwise it would obviously not work as each loop would start with a fresh object), then the reload time would be what takes time increasingly. But linearly not exponentially, and that would not explain much about the mouse "workaround".
That is one of the few things that need checking. But if I read your detailed operations correctly, that is not what you are doing.
0
 

Author Comment

by:davidcase
Comment Utility
The financial modelling application is our own. It was originally written about 12 years ago and it makes very heavy use of objects, factories, COM etc.

Initially, I agreed with Geert and thought my import routine sucked. but the routine only takes a couple of seconds to import over 120,000 cells of data from the excel sheet, format that data into a correctly formatted tree, save the output as an XML and create and open the plans parent object in the financial modeller. That did not seem bad.

In each iteration of the loop I create a new object, assign this object a parent object (hence the tree structure), update the objects information fields, and carry on with the loop.

I have disabled auto-recalc in the modeller.

I also liked the heavy book solution - and used it to create the demo imported plans for the customer. Sadly, the users would like something slightly more technical....

I have inserted code in the importer to locally create objects and update the data - very quick - no problems - so the problem must be the original application and how it handles lots and lots of objects thrown at it via COM.

However, that being said, I am still struggling to discover why holding the mouse key down on another window speeds the thing up by a vast amount...
0
 
LVL 45

Accepted Solution

by:
aikimark earned 500 total points
Comment Utility
What is the memory use during this process?

Maybe the performance difference is due to physical memory use and garbage collection.  If you press the mouse on another window, it forces the system to process messages and also forces your application to complete any Destroy() event.  I can envision a scenario where the Destroy() events are placed on the stack and only complete at the end of the export loops.  Once you get past some critical level, you start using virtual memory, which is MUCH slower.
 
0
 
LVL 36

Expert Comment

by:Geert Gruwez
Comment Utility
lol, davidcase, I'm sorry I suggested the approach
But i remember someone who actually used this technique in ... SAP !!!
For a more technical ... use a book like mathematical geometry  :)

enough with the jokes.

Maybe it's the garbage collector ?
COM objects are cleaned up when the garbage collector passes.
Maybe it isn't getting called unless something is making time (like hanging over a other app)

I have to admit, it's kinda weird to use a other app to speed up the process

And from what i read, it looks like a fantastic app, with some weird behaviour
doesn't mean the code sucks, probably windows itself messing up ... again
0
 
LVL 25

Expert Comment

by:epasquier
Comment Utility
If the financial application is yours, and you still have the code, then I suggest you start by adding some debug to it. Start with each main API functions, and get high performance counters at each start and end of functions, calc the difference, and sum all these in total counters specific to each functions.
That will tell you which is the one that is eating up all your CPU and behaves differently with or without the mouse trick.

When found the function that have the most problems, you can start digging in all the functions it uses the same way. I think that with a little time you will be able to pinpoint WHERE the problem lies. It would be then possible to think about WHY and HOW to fix it.

With this technique, I have been able to pinpoint one strange trouble with a complex plugin in yet a more complex application (retail application), both communicating with COM. So that is very similar. Other more traditional debugging where ineffective because it was impossible to trace all the calls back and forth between the main application and the plugin. So I believe that it is a very similar context

Here is an object specialized in performance counting that you can use. If you can try this approach and need more explanations about how it works, feel free to ask
Type

  TPerfCounter=class(TObject)

   private

    _Freq, _StartCount, _StopCount, _LastCount, _In, _Out,_LastInOut: Int64;

    function GetTimingSeconds: Double;

    function GetInOutPercent:Double;

    function GetInMs:Double;

    function GetOutMs:Double;

    function GetInOutMs:Double;

    procedure InOutProcess;

   public

    Constructor Create;



    procedure Start;

    procedure GetPerf;

    procedure InProcess;

    procedure OutProcess;

    function CountToS(c:Int64):Double;

    procedure AdjustInOut(c:Int64);



    property TimingSeconds: Double read GetTimingSeconds;

    property CountIn:Int64 read _In;

    property CountOut:Int64 read _Out;

    property LastInOut:Int64 read _LastInOut;

    property InMs:Double read GetInMs;

    property OutMs:Double read GetOutMs;

    property InOutMs:Double read GetInOutMs;

    property InOutPercent:Double read GetInOutPercent;

  end;



//==============================================================================

function TPerfCounter.GetInMs:Double;

begin

 Result:=1000*CountToS(_In);

end;



function TPerfCounter.GetOutMs:Double;

begin

 Result:=1000*CountToS(_Out);

end;



function TPerfCounter.GetInOutMs:Double;

begin

 Result:=1000*CountToS(_LastInOut);

end;



function TPerfCounter.GetTimingSeconds: Double;

begin

 if _StopCount<_StartCount Then GetPerf;

 Result := CountToS(_StopCount - _StartCount);

end;



function TPerfCounter.GetInOutPercent:Double;

begin

 if _In=0 Then Result:=0 Else Result:=100*_In/(_In+_Out);

end;



procedure TPerfCounter.InOutProcess;

Var

 _Count:Int64;

begin

 QueryPerformanceCounter(_Count);

 _LastInOut:=_Count-_LastCount;

 _LastCount:=_Count;

end;



//==============================================================================



Constructor TPerfCounter.Create;

begin

 QueryPerformanceFrequency(_Freq);

 _StartCount:=-1;

 _StopCount:=-1;

end;



procedure TPerfCounter.Start;

begin

 QueryPerformanceCounter(_StartCount);

 _LastCount:=_StartCount;

 _StopCount:=-1;

 _In:=0;

 _Out:=0;

end;



procedure TPerfCounter.GetPerf;

begin

 QueryPerformanceCounter(_StopCount);

end;



function TPerfCounter.CountToS(c:Int64):Double;

begin

 Result:=C/_Freq;

end;



procedure TPerfCounter.InProcess;

begin

 _In:=_In+_LastInOut;

end;



procedure TPerfCounter.OutProcess;

begin

 InOutProcess;

 _Out:=_Out+_LastInOut;

end;



procedure TPerfCounter.AdjustInOut(c:Int64);

begin

 Inc(_In,c);

 Dec(_Out,c);

end;

Open in new window

0
 
LVL 36

Expert Comment

by:Geert Gruwez
Comment Utility
epasquier ... you really need to update to delphi 2009 or delphi 2010.

when you do, check the new unit Diagnostics
it has TStopWatch ...

http://docwiki.embarcadero.com/VCL/en/Diagnostics.TStopwatch
0
 

Author Comment

by:davidcase
Comment Utility
I think we have kind of gone astray on the original question. all of the responses have been extremely productive and very much appreciated - however, can anyone shed any light on why the import speeds up when the left mouse button is held down on another application. If we could discover why this is then we may have a clue as to what is happening with the delays. Thanks everyone - much appreciated.
0
 
LVL 45

Expert Comment

by:aikimark
Comment Utility
@davidcase

Without more information from you about the results of the timing, the Delphi code, and the COM object code, we've probably helped you to the limit of our expertise.  More help at this time will require psychic abilities which we do not possess.

Please add the timing statements in the appropriate places and test your application without the mousedown trick and with the mousedown trick.  Post the results back in this thread and we will have more information to base comments and recommendations.
0
 
LVL 25

Expert Comment

by:epasquier
Comment Utility
@Geert : I will probably switch to newest Delphi before end of the year. I know I'll have a lot of new functionalities to catch up, I savour this idea already :o)

regarding TStopWatch, yes it looks pretty much the same as my first version of TPerfCounter. I then added some more functionalities to count In/out time of a particular complex multi-part processing compared to the global time spent. Said otherwise, it helps calculate the time spent in only a set of functions I want to track, excluding what might be around it.

Here is an example of how to use it

Var

 PerfCounter:TPerfCounter;



procedure IWantToTrackThisOnly;

begin

 PerfCounter.InProcess;

// do something

 PerfCounter.OutProcess;

end;



procedure GlobalProcess;

Var i:integer;

begin

 PerfCounter:=TPerfCounter.Create;

 PerfCounter.Start;

 for i:=0 to 10 do

  begin

    DoSomething;

    IWantToTrackThisOnly;

  end;

 With PerfCounter do 

  ShowMessageFmt('I spent %f ms in tracked functions out of %f ms total', [InMs, TimingSeconds ]);

 PerfCounter.Free;

end;

Open in new window

0
 
LVL 25

Expert Comment

by:epasquier
Comment Utility
By the way, I see now there was a bug in one of my methods :

procedure TPerfCounter.InProcess;

begin

 InOutProcess; // This line was missing ! how come I didn't noticed before ?

 _In:=_In+_LastInOut;

end;

Open in new window

0
 
LVL 36

Expert Comment

by:Geert Gruwez
Comment Utility
>>how come I didn't noticed before ?
you were in dire need of a holiday ?
0
 
LVL 36

Expert Comment

by:Geert Gruwez
Comment Utility
>>epasquier,
that code that brings up a old tool

davidcase have you ever heard of profiler ?
http://www.prodelphi.de/

what this tool does is inject debug code at the begin and end of every procedure/function/method
after recompiling and running you will be able to see a report of every millisecond spent by each proc/func/meth

i used it a few times (a decade ago)

you should be able to see what proc/func/meth is consuming the processor time
when you run it
A: normal
B: with the mouse down

comparing the 2 reports should show you which proc/func/meth is doing the dirty work ... :)
0
6 Surprising Benefits of Threat Intelligence

All sorts of threat intelligence is available on the web. Intelligence you can learn from, and use to anticipate and prepare for future attacks.

 
LVL 25

Expert Comment

by:epasquier
Comment Utility
And I see now that 'In' and 'Out' are reversed in my logic !
Not much of a problem, just call OutProcess when you enter and InProcess when you leave, or better yet exchange the code in those, but I'm wondering from what dust gathering folder did I get the version I posted ???
0
 
LVL 25

Expert Comment

by:epasquier
Comment Utility
David, Geert :
Yes, profiler is certainly the most finished tool along the same lines I just presented to you to help identifying the problem. The goal is the same : find what is taking that much time. Use whatever suits your needs and takes you less time to test, we are only interested in the results.

There is one thing that concerns me thought with Profiler, it might be too advanced on one point  :
"Source instrumenting also ensures that idle times caused by certain Delphi- or Windows- API-functions (e.g. Sleep, MessageBox, WaitForSingleObject etc.) are automatically excluded from measurement. "
I hope that is an option, and that you can test both with and without, and see if there is a difference when using the mouse trick
0
 
LVL 36

Expert Comment

by:Geert Gruwez
Comment Utility
oh yeah, when using that tool, be sure to take a backup of *all* your source files
that message will probably show somewhere in the tool too

it changes all your source files ...
0
 
LVL 25

Expert Comment

by:epasquier
Comment Utility
So it does not work as a precompiler ? too bad. It would have been great. I made a system like that in C, replacing all { and } delimiting procedures body by BEGIN and END macros, and while that looked a bit strange when looking at the code, just a compile option could switch code generation to normal application (BEGIN= { ) or debug mode ( BEGIN = { DEBUG_START_FUNC(FUNC_NAME); )

I still wish there could be some precompiler that could be set in Delphi, using macros or similar techniques, with some globals defines for the function name, code line etc... While it can be disturbing for a new coder arriving in a project using this, that opens loads of possibilities... Have you ever heard of such tools for Pascal/Delphi ? even if not integrated in Delphi IDE ?
0
 
LVL 45

Expert Comment

by:aikimark
Comment Utility
@davidcase

Thanks for the points.  What did your investigation reveal?
0
 
LVL 25

Expert Comment

by:epasquier
Comment Utility
Hi DavidCase, I'm surprised you closed this subject with not many responses to our askings for details.
Have you really found a solution ?

I'm interested in your findings. The answer from aikimark was very global, and if it proves to be true about the "mouse trick" leading to better memory management, i'd like to better understand that.

Would you care to explain what you have done ?
0
 

Author Comment

by:davidcase
Comment Utility
Hello Again,

I had previously tested the obvious items in the Delphi application to make absolutely certain that none of the delays were inside the delphi importer. I tried to make it clear that I was very happy with the general code but that I was experiencing delays when creating and updating objects in the COM server application.

As the application created object counts > 1000 it slowed down significantly - and got slower and slower until, after 2500 objects, it was taking almost 6 seconds to add 1 object to the COM server and update 6 properties.

The rest of the code shows no reduction in speed - it is "only" the COM stuff.

And, by accident, I discovered that by holding the left mouse button down on any other application that the COM stuff showed a really significant increase in speed.

I asked the question to see if anyone else had experienced that or had any ideas what may be causing it - even though any answer would be clearly generic without looking at the COM servers code itself, which is something I cannot share as this is a valuable and propiatory.

I did try to tell folks that the Delphi bit worked fine so running a profiler on it would achieve nothing.

Frankly, I think I just started to upset folks so I decided to close the question and retreat gracefully.    
0
 
LVL 36

Expert Comment

by:Geert Gruwez
Comment Utility
well...
the processexplorer would have shown what was being done on the system

i guess you didn't try processexplorer ?
if you open the lower pane, you can see all handles a app is using
you can even freeze the app, investigate what's it's more or less doing, and then unfreeze it again
almost debugging :)

feedback from using processexplorer would probably have pinned it down a little more

i didn't say your app sucked but >>"looks like a badly written import routine"
that could have been on both sides of the import

this stil leaves the problem running ?
0
 

Author Comment

by:davidcase
Comment Utility
Yup - still no real solution, but I am exploring more using the SysInternals stuff (been using it for years - love it). Basically we "know" that the object update routines in the main application (COM Server) really do suck as the update a great deal of values internally. I turn autocalc off in the main application which helps a little - but no real improvement.

In the meantime I am looking at memory loads etc but still no idea why the depressed mopuse button would make a difference.

I guess I was hoping that someone else had seen this behaviour before...
0
 
LVL 36

Expert Comment

by:Geert Gruwez
Comment Utility
have you tried building in a pause after ... 100 records processing
does the memory usage go down after a certain pause time ?
0
 
LVL 45

Expert Comment

by:aikimark
Comment Utility
@davidcase

From your most recent description (above), I would guess that the slow down is caused by one of three likely culprits.
1. Each newly added item requires some string concatenation
2. Each newly added item requires a sequential iteration of the existing items in order to locate the end of the linked list.
3. (related to #1) Each newly added item requires some memory management operation that has a queued clean-up.

================
There was no sniping or inter-expert sniping in this thread.  We can reopen this question if you like to explore this further.

I think that #3 is the leading candidate for me, given the other-application mouse trick effect.  It could be the Delphi code, allocating a new variable/object, instead of reusing the object.  It could also be a problem of your available physical memory.  If either the Delphi application or the COM object uses a lot of memory, you may reach a point where your process begins to use virtual memory.
0
 
LVL 45

Expert Comment

by:aikimark
Comment Utility
I helped one of my Delphi user group members post a performance question about TClientDataset population (Insert or Append).  Once you get past 20k inserts, you start to see the loop slow down.  After 50k inserts, the performance starts to crawl.

The underlying problem with this Delphi component is memory management.  Even applying the FastMM memory manager, this process is slow.  Concatenation can kill even intrinsic components.
0
 
LVL 36

Expert Comment

by:Geert Gruwez
Comment Utility
aikimark ... you are thinking exactly as me
but you have a better way with words

i would think that the process waits for some idle time to do cleanup
that's why i hinted to pause the processing on the delphi side ...
 ... and also a comment above with the garbage collector

what the mouse and a other app has to with this ... maybe focus ??
0
 
LVL 45

Expert Comment

by:aikimark
Comment Utility
@Geert

My first thoughts and comments were GC-related.

However, we can't rule out a part that the Delphi code might play in this.  A long time ago, one of my first Windows applications exhibited a strange behavior.  When the application ran as the user would see it, I could see the anomaly.  When I ran it in debug mode and stepped through the events, I couldn't see the behavior.  Turns out that events were being queued at run time, but were invoked in a different order when I stepped through the code in debug mode.

We know too little about the participants in this little dance to provide a definitive answer to the mouse-trick question.

I do agree that the pause is worth trying.  I would also recommend
     Application.ProcessMessages
in the output (to COM object) loop.
0
 
LVL 25

Expert Comment

by:epasquier
Comment Utility
David, I understand your concerns, but really we are still willing to help you on this one.
But I still think some hard data measurements needs to be done here, to pinpoint what exact piece of code is causing a problem. So IMO you still should use performance tools, but in your COM server - now that we all agree that the problem is not your application.

And yes, you can start the performance tracing with the functions that are likely doing some heavy work that can be impacted with the growing size of the data you are manipulating. At some point, maybe you will be able to produce a very small bit of code that will be sharable
0
 

Author Comment

by:davidcase
Comment Utility
aikimark is very close to describing our problem exactly.

The main application adds objects and properties to an internal tree structure (and we all know how slow they can be). Each new object added causes a refresh of the tree and worse - each numerical property value added causes the entire thing to update the internal chart of accounts. This app was originally coded by an ex MS guy who worked on the COM stuff for MS - hence it is horribly powerful but subclasses MFC and the like all over the place and does things with COM that are really weird and, quite honestly, almost impossible to understand. (comments would have been nice)...

So I know, and wholeheartedly agree, that the actual import code after we send the object and properties to the main application is the culprit - just cant help wondering why that mouse makes such a huge difference. We go from one object added every 6 seconds to 2 objects every second.

And yes, I have authorised the update of the main application code to VS2010 as a first step - and then we can start a coordinated rewrite - I hope.

Folks, I really appreciate your help and please forgive my frustration because this is driving me nuts.

I have not tried FastMM because I was under the impression that the memory manager in D2009 was as good as or better than FastMM - have I got that wrong...


0
 
LVL 45

Expert Comment

by:aikimark
Comment Utility
FastMM was incorporated in to the base code.  I mentioned it because my user group member was using an older version of Delphi.

Any time you are doing a bulk update, you should disconnect/suspend any screen (visible control) updating.
0
 
LVL 25

Expert Comment

by:epasquier
Comment Utility
>> Folks, I really appreciate your help and please forgive my frustration because this is driving me nuts.
Completely understandable...

It would seem that a complete redesign is in order, including some system to disable any kind of unnecessary calculations while adding data, plus a function to commit all in one pass.
Like BeginUpdate / EndUpdate

You have my best wishes for luck
0
 
LVL 45

Expert Comment

by:aikimark
Comment Utility
my offer to reopen the question is still valid.  I have only offered speculation.  The other two experts can offer code fixes, especially if the COM object is written in Delphi.
0
 
LVL 45

Expert Comment

by:aikimark
Comment Utility
yes. (what epasquier wrote)
0

Featured Post

How your wiki can always stay up-to-date

Quip doubles as a “living” wiki and a project management tool that evolves with your organization. As you finish projects in Quip, the work remains, easily accessible to all team members, new and old.
- Increase transparency
- Onboard new hires faster
- Access from mobile/offline

Join & Write a Comment

Suggested Solutions

Title # Comments Views Activity
bigDiff challenge 17 75
fix34  challenge 9 95
matchUp  challenge 9 71
LAN or WAN ? 11 58
Introduction: Ownerdraw of the grid button.  A singleton class implentation and usage. Continuing from the fifth article about sudoku.   Open the project in visual studio. Go to the class view – CGridButton should be visible as a class.  R…
Exception Handling is in the core of any application that is able to dignify its name. In this article, I'll guide you through the process of writing a DRY (Don't Repeat Yourself) Exception Handling mechanism, using Aspect Oriented Programming.
THe viewer will learn how to use NetBeans IDE 8.0 for Windows to perform CRUD operations on a MySql database.
The viewer will learn how to use and create keystrokes in Netbeans IDE 8.0 for Windows.

743 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

13 Experts available now in Live!

Get 1:1 Help Now