I’m writing this article about coding issues at a time when I am winding down a contract I am working on. When I do, I think about the state of the code when I joined the team and the state when I leave. I work very hard to make sure that I leave the code, project, and team in a better state from when I joined. For this contract, I was hired to specifically work on performance and code quality. This project suffers from the same coding issues that I see repeatedly as a contractor. The goal of these articles is to give you some statistics and to highlight just some of the issues that I see in most every project that I work on.
This article is turning out to be full of so much of this information that I want to share, that I’m going to have to break it up into smaller more consumable bites. I hope you read all the articles in this series so you too can improve the performance and memory issues along with the common code issues that I see in the projects where I work. Most of the code samples are from this project along with some from my OOS project Spargine and some from ChatGPT.
The Statistics
I was curious if all the work I’ve done for this project really did help the quality of the code. What I did was to take the comprehensive EditorConfig file that I use for all my projects (link at the end of this article) and apply it to the code the day I started, using Analyze in Visual Studio and compare it to the code the day I left the contract. The code I had in the pull-request process when I left was moving the solution to .NET 7 so that we could take advantage of some of the great performance improvements including code generators.
The statistics I looked at were the number of classes in the solution, members, cyclomatic complexity, maintainability index, depth of inheritance, class coupling, lines of source code, lines of executable code, lines of cloned code, unit tests, code errors, code warnings, and code messages. Most of the statistics are moving in the right direction. Of course, I wished the numbers were better, but am happy with most of them. I do wish the code errors (3,677) and warning numbers (6,426) were better.
This code base has been around since the mid-2000s, so like with most older solutions it suffered from a lot of "dead code". I spent a lot of time cleaning up this dead code and there is more to do, but I was able to remove 883 members, almost 45K lines of source code, and 1,309 lines of cloned code. But there is one statistic that I write and speak about a lot because I see it in every codebase I work on.
Cyclomatic Complexity
I’ve been asking attendees at my coding standard conference session for almost twenty years "What is cyclomatic complexity and what does the number really mean?" Most engineers have a hard time explaining cyclomatic complexity off the top of their heads, but it’s basically the number of paths that a method can take until it is completed. The higher the number, the more paths are taken. You must strive to keep this number low. I try to keep the number under 20. Much of the information online says the maximum number should be 25. Higher numbers make more complex code and makes it less reusable.
But what this number really means to me is that it’s the minimum number of unit tests you need to write just to test the encapsulation of the method! Many methods in this code base had numbers north of 100. So that means over 100 unit tests just to test encapsulation. I’ve never seen that many unit tests for a single method! From my analysis, the code base needs 76,479 more-unit tests at the minimum.
There isn’t a company in the world that will stop development just to add them. So, it’s very important to ensure that unit tests are written at least for every public and protected method in the code base. Since this code base is severely lacking in unit tests, I advised the team to not only make sure they are there for new code, but to add them of the methods that are missing them as they are modified.
Next, I will explain more of the common issues that I see over and over along with some performance gains I was able to add.
Memory Issues
I hate to say this, but every single project I worked on as a consultant had memory issues, some severe, and this solution was no different. They had the typical issues of having to reboot backend services due to these issues along with crashing issues. So, this was one of the first things that I tackled.
The other problem that the memory issues were causing was severely restricting the number of concurrent users they could handle along with the number of hits per second. Because of the new customers they were bringing into their solution, the company wanted to handle around 40K concurrent users. When I joined, the solution only handled around 100. When I left that number was up to around 7K with a max requests per second of 250. This increase was due to all the work I was able to get into the main branch, along with work on other areas by other developers. But as you can see, they still have a lot of work to do to meet the business requirements.
These memory issues were caused by not properly disposing of disposable objects and not properly implementing the IDisposable pattern. I write about these issues extensively in my coding standard book and in my multipart series of articles titled "Everything That Every .NET Developer Needs to Know About Disposable Types" that I hope you and your team will read by going to this link: https://bit.ly/DisposeArticles.
Performance Improvements
As discussed in the section above, the performance of this solution did not meet the business requirements and I worked very hard on these problems. Many of these issues will be explained in all the articles of this series, but I’d like to start off with some I did due to improvements to .NET.
Logging
Every project I work on does not have enough logging. Logging issues, events and more is the only way that issues can be discovered and fixed. Without it, finding and fixing these issues takes much longer and will be more expensive. So, it’s critically important that this is done in all the code. It’s also critical that logging does not degrade performance as much as possible.
Step in .NET 6 and source generators! Using partial classes and the LoggerMessage attribute, you can bring down the execution time of over 20-30 nanoseconds down to 3-4. Wow, totally worth putting in the work. I’ve already written about this in my article titled "Speed Up Logging In .NET 6" which I hope you will read by going to this link: https://bit.ly/LoggingNet6.
StringBuilder
If you have been using .NET for a while, then you know that in most cases, using the StringBuilder
improves the performance of concatenating strings. But did you know that there is a way to make the StringBuilder
even faster? This is done by using the StringBuilder
from an ObjectPool
from a private field in a class. For now, and in the future, I recommend using the StringBuilder ObjectPool
until something more performant comes along. I have written about this in my article titled "Speeding Up the StringBuilder Using an ObjectPool" that I hope you will read by going to this link: https://bit.ly/ObjectPoolStringBuilder.
Summary
In this article I explained some common issues that I see in just about every team and solution that I work on. But I am far from done! The following articles will be full of issues I see all the time along with code examples and detailed descriptions.
You can find more valuable help by picking up a copy of my book "Rock Your Code: Coding Standards for Microsoft .NET" available on Amazon.com. You can find more performance tips for .NET by picking up the 3rd edition of "Rock Your Code: Code & App Performance for Microsoft .NET" available on Amazon.com.
I hope you will incorporate the settings in my EditorConfig file to analyze your code the same way that I did for these articles. You can find it at this link: https://bit.ly/dotNetDaveEditorConfig. I update it quarterly, so make sure to update yours too!
Please make a comment below. I’d love to hear from you.
Read More about Real World Coding Issues - Style and Performance Issues