A few months ago, I needed to do some calendar programming for work, and I came across the dday.ical library, like many developers before me. And like many developers, I discovered that dday.ical doesn’t have the best performance, particularly under heavy server loads.
I dug in, and started making changes to the source code, and that’s when I discovered that the licensing was ambiguous, and that it had been abandoned. I was concerned that I might be exposing my company to risk due to unclear copyright, and a non-standard license.
dday.ical is now ical.net
mdavid, who saw to it that the library wasn’t lost to the dustbin of Internet history, has graciously redirected dday users to ical.net. Khalid Abuhakmeh, who published the dday nuget package that you might be using (you should switch ASAP) has also agreed to archive and redirect users to ical.net.
So… why should you use the new package?
Doug has revoked his copyright, and given unrestricted permission to give dday.ical new life as ical.net. That means ical.net is unencumbered by legal ambiguities.
Many performance enhancements
My changes to ical.net have been mostly performance-focused. I was lucky in that dday.ical has always included a robust test suite with about 170 unit tests that exercise all the features of the library. Some were broken, or referenced non-existent ics files, so I nuked those right away, and concentrated on the set of tests that were working as a baseline for making safe changes.
- Old dday.ical test suite: ~17 seconds
- Latest ical.net nuget package: 3.5 seconds
There’s no games here. ical.net really is that much faster.
Profiling showed a few hotspots which I attacked first, but those only bought me maybe 3-4 seconds improvement. There was no single thing that resulted in huge performance gains. Rather it was many, many small changes that contributed, quite often by improve garbage collection pauses, many of which were 5ms+, which is an eternity in computing time.
Here are a few themes that stand out in my memory:
- Route all time zone conversions through NodaTime, which actually exposed some bugs in what the unit tests were asserting
- Converting .NET 1.1 collections (
ArrayList) to modern, generic equivalents
HashSet<T>for many collections, including creating stable, minimal
GetHashCode()methods, though more attention is still needed in this area. A nice side effect of this was that lot of lookups and collection operations then became set operations (
- Converting several
O(n)or better by restructuring methods based on information that was available in context
- Converted a lot of loops to LINQ. (Yes, really!)
- Specifying initial collection sizes when using array-backed collections like
- Moved variables closer to their usage, which sometimes meant that certain expensive calls don’t occur at all, because the method exits before reaches it. This also had the effect of pushing some variables into gen 0 garbage collection. (Anecdotally, I have noticed GC pauses are fewer and further between, though I don’t have any hard data that it’s actually significant.)
- Moving expensive calls outside of tight loops. Unfortunately the library makes extensive use of the service-provider antipattern. A common thing was to have an expensive call (get me a deserializer for
Foo!) inside a tight loop that’s only ever deserializing
Foos. So you can make the call once and just reuse the deserializer.
- Implemented a lazy caching layer as suggested in one of the
TODOs in the comments.
Along the way, I converted a lot of code to modern, idiomatic C#, which actually helped performance as much as any of the discrete things I did above. As I work towards a .NET Core port, I have the runtime down to about 2.8 seconds just through clarifying and restructuring existing code, and idiomatic simplifications.
- A .NET Core port is nearly complete.
- The ical.net has virtually no documentation. I hope to improve the readme with some simple examples this morning/afternoon.
- I have been bug collecting on Stack Overflow, and have a few maybe-bugs to investigate and/or write test cases for.
- Maybe some API changes for v3, still TBD. I’ll discuss these in a future blog post.