question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Improve performance for heavy users

See original GitHub issue

In our project, we heavily use (I can hear you think “abuse”) Quartz.NET. We actually use Quartz.NET more as a thread scheduler than a job scheduler. We have a lot of jobs, with multiple instances for a given job and short intervals between executions. We have no need for persistence, so we only use RAMJobStore.

We’re running on .NET Framework 4.8, but planning for a migration to .NET 5+. One step in that direction was to upgrade Quartz.NET from version 2.x to version 3.x. We did a POC for this, and learned that the upgrade came with a cost. I suspect part of this cost comes from the async machinery, but will need a little more time to dig into this. The async story has improved a lot on .NET (Core), but we cannot upgrade to .NET (Core) in a single pass.

This issue is NOT to complain about slower performance. We’re extremely grateful for all the work you have been doing, so let me just say: Thanks!!

I hope to use this issue to:

  • raise awareness of the impact that an upgrade from 2.x to 3.x (and probably 4.x) has for those projects making heave use of Quartz.NET.
  • inform and learn about areas in Quartz.NET that could use some “love”.
  • help you (and ourselves) to improve performance and reduce allocations, where possible.

RAMJobStore

When I learned of the reduced performance (and higher CPU usage) of Quartz.NET, I did a quick scan of the (Quartz.NET) code base to look for opportunities to improve performance.

I identified improvements to RAMJobStore (for which I’ll submit a PR) that can be categorized as:

  • Where possible do stuff outside of the instance lock, hereby moving the needle (just a little) for concurrency.
  • Avoid virtual calls as these are a lot more costly than direct calls.
  • Do not use RetrieveJobInternal(JobKey jobKey) to only check if a job exists as that method comes with an extra cost due to the cloning of IJobDetail.
  • Prefer for loops over foreach to avoid cost of allocating and disposing an enumerator.
  • Avoid using ConcurrentDictionary for a dictionary that is only ever used while owning the instance lock.

I do not expects these changes to resolve all our issues. Baby steps …

Future

Once my work on RAMJobStore is done, I’ll scan for other areas where improvements can be realised. If there are areas that are known to be less optimal (time is a limited resource for everyone), I’d be happy to take a stab at improving these.

Issue Analytics

  • State:open
  • Created 2 years ago
  • Comments:5 (5 by maintainers)

github_iconTop GitHub Comments

4reactions
driesengcommented, Nov 19, 2021

To see where we’re at right now, I used an improved version of SchedulerBenchmark (which I’ll soon create a PR for) against the tip of the v4 branch and the commit before I started improving Quartz.NET performance (with high-precision SimpleTriggerImpl changes on top).

Before

Method Job UnrollFactor Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
DisableConcurrent_15Threads_15Jobs_1TriggerPerJob DefaultJob 16 6.673 us 0.0277 us 0.0216 us 1.0040 - - 4.07 KB
DisableConcurrent_15Threads_15Jobs_5TriggersPerJob DefaultJob 16 12.446 us 0.1087 us 0.0964 us 1.4080 0.0020 - 5.71 KB
DisableConcurrent_15Threads_30Jobs_1TriggerPerJob DefaultJob 16 6.045 us 0.1146 us 0.1490 us 1.0100 0.0020 - 4.1 KB
DisableConcurrent_50Threads_15Jobs_1TriggerPerJob DefaultJob 16 6.667 us 0.0003 us 0.0003 us 1.0060 - - 4.07 KB
DisableConcurrent_50Threads_30Jobs_1TriggerPerJob DefaultJob 16 4.306 us 0.0579 us 0.0541 us 1.0000 - - 4.06 KB
DisableConcurrent_50Threads_30Jobs_3TriggersPerJob DefaultJob 16 8.120 us 0.0612 us 0.0543 us 1.1820 - - 4.79 KB
DisableConcurrent_50Threads_30Jobs_30TriggersPerJob DefaultJob 16 95.332 us 0.9026 us 0.8001 us 4.0080 0.0180 0.0040 16.25 KB
Concurrent_15Threads_15Jobs_1TriggerPerJob DefaultJob 16 6.667 us 0.0001 us 0.0000 us 0.9460 - - 3.84 KB
Concurrent_15Thread_30Jobs_1TriggerPerJob DefaultJob 16 5.001 us 0.0956 us 0.1909 us 0.9400 - - 3.81 KB
Concurrent_50Threads_15Jobs_1TriggerPerJob DefaultJob 16 6.667 us 0.0002 us 0.0002 us 0.9460 - - 3.83 KB
Concurrent_50Threads_30Jobs_1TriggerPerJob DefaultJob 16 3.942 us 0.0428 us 0.0400 us 0.9420 0.0020 - 3.81 KB
Concurrent_30Threads_15Jobs_1TriggerPerJob_RepeatCountZero Job-EMKSJS 1 12.099 us 0.3387 us 0.9607 us - - - 4.52 KB
Concurrent_30Threads_15Jobs_2TriggerPerJob_RepeatCountZero Job-EMKSJS 1 10.733 us 0.4227 us 1.2465 us - - - 4.5 KB

After

Method Job UnrollFactor Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
DisableConcurrent_15Threads_15Jobs_1TriggerPerJob DefaultJob 16 6.667 us 0.0001 us 0.0001 us 0.7980 - - 3.23 KB
DisableConcurrent_15Threads_15Jobs_5TriggersPerJob DefaultJob 16 8.549 us 0.1284 us 0.1138 us 1.0240 0.0020 - 4.15 KB
DisableConcurrent_15Threads_30Jobs_1TriggerPerJob DefaultJob 16 5.153 us 0.1029 us 0.2007 us 0.7940 - - 3.22 KB
DisableConcurrent_50Threads_15Jobs_1TriggerPerJob DefaultJob 16 6.667 us 0.0001 us 0.0001 us 0.8040 0.0020 - 3.26 KB
DisableConcurrent_50Threads_30Jobs_1TriggerPerJob DefaultJob 16 3.483 us 0.0568 us 0.0531 us 0.7920 - - 3.21 KB
DisableConcurrent_50Threads_30Jobs_3TriggersPerJob DefaultJob 16 5.947 us 0.1097 us 0.1026 us 0.9000 0.0020 - 3.65 KB
DisableConcurrent_50Threads_30Jobs_30TriggersPerJob DefaultJob 16 72.458 us 0.9462 us 0.8850 us 2.1740 0.0100 0.0040 8.8 KB
Concurrent_15Threads_15Jobs_1TriggerPerJob DefaultJob 16 6.667 us 0.0001 us 0.0001 us 0.7840 - - 3.18 KB
Concurrent_15Thread_30Jobs_1TriggerPerJob DefaultJob 16 4.368 us 0.0845 us 0.1264 us 0.7740 - - 3.14 KB
Concurrent_50Threads_15Jobs_1TriggerPerJob DefaultJob 16 6.667 us 0.0001 us 0.0000 us 0.7840 - - 3.18 KB
Concurrent_50Threads_30Jobs_1TriggerPerJob DefaultJob 16 3.382 us 0.0502 us 0.0469 us 0.7740 0.0020 - 3.14 KB
Concurrent_30Threads_15Jobs_1TriggerPerJob_RepeatCountZero Job-ICANYR 1 10.804 us 0.2901 us 0.8322 us - - - 3.76 KB
Concurrent_30Threads_15Jobs_2TriggerPerJob_RepeatCountZero Job-ICANYR 1 9.495 us 0.4006 us 1.1748 us - - - 3.78 KB

Observations:

  • Memory allocations have dropped a good deal.
  • There’s a good increase of throughput. This increase is more obvious for those benchmarks that have multiple triggers for a given job.
  • It’s difficult to implement non-artificial benchmarks that show the effects on the scheduler. I have some ideas on how to improve this.

Next steps:

  • Reduce memory allocations in QuartzSchedulerThread.
  • Reduce contention in ListenerManagerImpl (pending discussion #1384).
  • Consider removing support for internal trigger listeners.
  • Consider using Dictionary<TKey,TValue> for triggersByKey in RAMJobStore.
  • Improve performance of JobExecutionContextImpl (via improvements to JobDataMap, …?).
  • Improve performance of building and enumerating list of listeners in QuartzScheduler.
  • Move checking for PersistJobDataAfterExecutionAttribute and DisallowConcurrentExecutionAttribute to JobBuilder which changes this to a one-time cost only to be pair during setup (pending discussion #1412).

Some of these changes should have an even greater impact on performance, concurrency and memory allocations.

1reaction
driesengcommented, Oct 20, 2021

Thanks for the fast feedback!!

I do not plan to introduce any breaking changes, even though - as you mentioned - changing some methods to return concrete types, or take concrete types as arguments might surely be beneficial.

Changing from IEnumerable<T> to List<T>, for example, may be great for perf but from a consumer point of view you’re moving from something that has the clear intent of being read-only to something which is clearly not.

Let’s start with small harmless changes for 4.x, and take it from there. I’ll also try to find time to write benchmarks that we can run against 2.x and 4.x.

The change I proposed to replace the last occurrence of ConcurrentDictionary<TKey,TValue> with Dictionary<TKey,TValue> has already dropped dead in the water:

  • Dictionary<TKey,TValue>.Remove(TKey key, out TValue value) is only supported on .NET Standard 2.1 or higher, so not on any version of .NET Framework. This means I would have to replace a single call to ConcurrentDictionary<TKey,TValue> with a call to Dictionary<TKey,TValue>.TryGetValue(TKey key, out TValue value) and Dictionary<TKey,TValue>.Remove(TKey key).
  • I missed a few places where the dictionary in question (triggersByKey) is used outside of the instance-level lock.

We do miss a few opportunities where we could delay or even avoid acquiring the instance-level lock.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Website Speed Optimization: 14 Tips to Improve Performance
Learn how to improve website performance. ... Heavy CSS and JavaScript use; Poor server/hosting plan; Large image sizes; Not using browser ...
Read more >
Improve performance in After Effects
By far, the best way to improve performance overall is to plan ahead, run early tests of your workflow and output pipeline, and...
Read more >
Fast load times - web.dev
Performance plays a significant role in the success of any online venture, as high performing sites engage and retain users better than poorly...
Read more >
Five Data-Loading Patterns To Boost Web Performance
Efficient use of a client's resources and bandwidth can greatly improve your application's performance.
Read more >
Optimize for User Traffic
The most effective way of optimizing for user traffic is to adjust the number of VizQL server processes. Add one VizQL server process...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found