How to look for high complexity parts
I want to visualize the messiness of code
When I usually develop with Ruby, I sometimes encounter code that I find difficult to read. I wish I could refactor it freely, but software development at work is often aimed at maximizing profit for a specific period of time, so the investment ratio between product development and debt return is often a tradeoff between business impact and difficulty of development.
If the product will be finished in a few months, there may be a decision to keep going even if it is hard to read. On the other hand, if the company plans to put more effort into the product in the future, refactoring may be a more successful investment because it maximizes development efficiency * development time
.
The quality of code and the code to be refactored is often talked about in terms of ambiguity. It would be better to have a certain measure of success to make the discussion with confidence.
I introduce Flog this time because it seems to make such triage easier.
Install Flog
You can use Sinatra or Hanami as long as the code is in Ruby, but since I think this is the most common use case, I will proceed on the assumption that you are using Ruby on Rails in your web development. Redmine code(commit: e8db9db8948d928ab8e9dae75c261967676291a8
) is used.
Install Flog.
# Gemfile group :developmentdogem"flog"end
bundle install
Run flog. I think that app
or lib
will be used in most cases when dir is specified.
$ bundle exec flog app/ 38137.0: flog total 15.5: flog/method average 456.0: ApplicationHelper#parse_redmine_links app/helpers/application_helper.rb:1088-1284 348.4: Query#sql_for_field app/models/query.rb:1218-1446 264.4: IssuesHelper#show_detail app/helpers/issues_helper.rb:524-658 251.6: Issue#none 189.4: User#none 185.1: Project#none 172.7: IssueImport#build_object app/models/issue_import.rb:113-241 167.9: Project#copy_issues app/models/project.rb:1122-1225 162.9: IssueQuery#initialize_available_filters app/models/issue_query.rb:147-280 160.8: Principal#none 158.8: IssuesController#bulk_edit app/controllers/issues_controller.rb:257-341 144.4: WorkflowTransition::replace_transitions app/models/workflow_transition.rb:23-91 141.3: Repository::Cvs#fetch_changesets app/models/repository/cvs.rb:128-200 136.9: ActivitiesController#index app/controllers/activities_controller.rb:25-85 136.0: Issue#validate_issue app/models/issue.rb:751-815 131.1: Issue#safe_attributes= app/models/issue.rb:555-633 128.0: IssuesHelper#render_descendants_tree app/helpers/issues_helper.rb:92-144 (the rest continues)
In this example, flog score under app is output.
flog score, class name#method name, file name, and line number are output. flog total is the total score and average is the average.
Internally, flog score seems to measure Abc size. By default, only top 60% is displayed.
Flog threshold change, grouping
Thresholds can be optionally changed.
$ bundle exec flog -t 10 app/ # top 10% 38137.0: flog total 15.5: flog/method average 456.0: ApplicationHelper#parse_redmine_links app/helpers/application_helper.rb:1088-1284 348.4: Query#sql_for_field app/models/query.rb:1218-1446 264.4: IssuesHelper#show_detail app/helpers/issues_helper.rb:524-658 251.6: Issue#none 189.4: User#none 185.1: Project#none 172.7: IssueImport#build_object app/models/issue_import.rb:113-241 167.9: Project#copy_issues app/models/project.rb:1122-1225 162.9: IssueQuery#initialize_available_filters app/models/issue_query.rb:147-280 160.8: Principal#none 158.8: IssuesController#bulk_edit app/controllers/issues_controller.rb:257-341 144.4: WorkflowTransition::replace_transitions app/models/workflow_transition.rb:23-91 141.3: Repository::Cvs#fetch_changesets app/models/repository/cvs.rb:128-200 136.9: ActivitiesController#index app/controllers/activities_controller.rb:25-85 136.0: Issue#validate_issue app/models/issue.rb:751-815 131.1: Issue#safe_attributes= app/models/issue.rb:555-633 128.0: IssuesHelper#render_descendants_tree app/helpers/issues_helper.rb:92-144 124.4: Query#statement app/models/query.rb:969-1026 123.9: AccountController#lost_password app/controllers/account_controller.rb:56-132 121.0: QueriesHelper#retrieve_query app/helpers/queries_helper.rb:345-395 118.9: IssuesController#bulk_update app/controllers/issues_controller.rb:343-424
Grouping (and sorting) by class is also possible.
$ bundle exec flog -g app/ 38137.0: flog total 15.5: flog/method average 1735.8: Issue total 251.6: Issue#none 136.0: Issue#validate_issue app/models/issue.rb:751-815 131.1: Issue#safe_attributes= app/models/issue.rb:555-633 112.3: Issue#after_create_from_copy app/models/issue.rb:1728-1797 102.4: Issue#recalculate_attributes_for app/models/issue.rb:1826-1877 61.9: Issue#project= app/models/issue.rb:413-452 61.6: Issue#workflow_rule_by_attribute app/models/issue.rb:685-732 60.5: Issue#validate_required_fields app/models/issue.rb:818-837 59.8: Issue#new_statuses_allowed_to app/models/issue.rb:1052-1093 52.7: Issue::visible_condition app/models/issue.rb:131-163 45.4: Issue#reschedule_on! app/models/issue.rb:1389-1423 43.1: Issue::update_versions app/models/issue.rb:1883-1899 42.6: Issue::load_visible_relations app/models/issue.rb:1227-1245 41.3: Issue#would_reschedule? app/models/issue.rb:1321-1338 41.2: Issue#css_classes app/models/issue.rb:1452-1466 37.5: Issue#after_project_change app/models/issue.rb:1697-1723 36.3: Issue::load_visible_last_notes app/models/issue.rb:1277-1294 35.9: Issue#notified_users app/models/issue.rb:1115-1129 34.7: Issue#copy_from app/models/issue.rb:295-323 34.6: Issue::cross_project_scope app/models/issue.rb:1535-1560 34.2: Issue#create_parent_issue_journal app/models/issue.rb:2007-2031 33.8: Issue#visible? app/models/issue.rb:166-188 33.0: Issue::load_visible_total_spent_hours app/models/issue.rb:1214-1224 32.6: Issue::load_visible_last_updated_by app/models/issue.rb:1258-1274 28.0: Issue#valid_parent_project? app/models/issue.rb:1517-1532 27.2: Issue::allowed_target_trackers app/models/issue.rb:1659-1676 26.1: Issue#visible_journals_with_index app/models/issue.rb:909-925 25.8: Issue#behind_schedule? app/models/issue.rb:974-979 25.6: Issue#assignable_versions app/models/issue.rb:999-1016 23.8: Issue::count_and_group_by app/models/issue.rb:1598-1617 23.3: Issue#tracker= app/models/issue.rb:379-393 1578.2: ApplicationHelper total 456.0: ApplicationHelper#parse_redmine_links app/helpers/application_helper.rb:1088-1284 113.6: ApplicationHelper#parse_wiki_links app/helpers/application_helper.rb:974-1038 85.3: ApplicationHelper#progress_bar app/helpers/application_helper.rb:1570-1602 76.8: ApplicationHelper#none 68.4: ApplicationHelper#format_object app/helpers/application_helper.rb:254-313 59.6: ApplicationHelper#render_page_hierarchy app/helpers/application_helper.rb:453-481 57.9: ApplicationHelper#page_header_title app/helpers/application_helper.rb:744-770 57.6: ApplicationHelper#render_project_nested_lists app/helpers/application_helper.rb:422-451 53.9: ApplicationHelper#textilizable app/helpers/application_helper.rb:848-893 53.0: ApplicationHelper#replace_toc app/helpers/application_helper.rb:1434-1465 43.7: ApplicationHelper#render_projects_for_jump_box app/helpers/application_helper.rb:533-568 42.9: ApplicationHelper#parse_non_pre_blocks app/helpers/application_helper.rb:895-925 42.8: ApplicationHelper#link_to_principal app/helpers/application_helper.rb:58-79 42.6: ApplicationHelper#project_tree_options_for_select app/helpers/application_helper.rb:595-616 41.1: ApplicationHelper#parse_inline_attachments app/helpers/application_helper.rb:936-962 40.8: ApplicationHelper#body_css_classes app/helpers/application_helper.rb:818-833 36.4: ApplicationHelper#render_project_jump_box app/helpers/application_helper.rb:571-593 35.8: ApplicationHelper#principals_options_for_select app/helpers/application_helper.rb:644-661 33.9: ApplicationHelper#link_to_issue app/helpers/application_helper.rb:104-122 32.0: ApplicationHelper#include_calendar_headers_tags app/helpers/application_helper.rb:1633-1659 28.3: ApplicationHelper#principals_check_box_tags app/helpers/application_helper.rb:625-641 27.2: ApplicationHelper#inject_macros app/helpers/application_helper.rb:1412-1429 25.4: ApplicationHelper#parse_headings app/helpers/application_helper.rb:1352-1373 23.1: ApplicationHelper#title app/helpers/application_helper.rb:773-783 1433.4: Query total 348.4: Query#sql_for_field app/models/query.rb:1218-1446 124.4: Query#statement app/models/query.rb:969-1026 104.3: Query#project_statement app/models/query.rb:935-967 (the rest continues)
There does not seem to be an option to summarize by class name only, so grep is used. (Actually I would like to know the average rather than the total if possible.)
$ bundle exec flog -g app/ |grep total |grep ':' 38137.0: flog total 1735.8: Issue total 33.0: Issue::load_visible_total_spent_hours app/models/issue.rb:1214-1224 1578.2: ApplicationHelper total 1433.4: Query total 27.8: Query#total_with_scope app/models/query.rb:1099-1112 1036.1: IssuesController total 917.1: IssueQuery total 909.9: IssuesHelper total 757.1: MailHandler total 732.5: Project total 657.5: User total 564.8: QueriesHelper total 24.9: QueriesHelper#available_totalable_columns_tags app/helpers/queries_helper.rb:113-128 442.4: RepositoriesController total 424.5: ApplicationController total 408.6: Mailer total 354.4: UsersController total 344.9: WikiController total 312.9: TimelogController total 311.4: RepositoriesHelper total 305.8: TimeEntry total 303.0: Attachment total 295.5: ProjectsController total 276.9: AccountController total 275.6: IssueImport total 254.2: Journal total 244.6: Repository::Cvs total 239.5: TimeEntryQuery total 232.0: Repository total 225.6: VersionsController total 205.2: Principal total 189.1: Version total 180.2: Setting total 174.1: WatchersController total 167.3: WorkflowsController total 166.8: Changeset total 156.7: WikiPage total 152.0: SearchHelper total 151.5: CustomField total 145.6: AuthSourceLdap total 144.4: WorkflowTransition total 143.0: Import total (the rest continues)
Redmine is project management software. It is natural that the code around issues, projects and emails will be the majority. Data analysis often produces results that fit the obvious intuition.
It would be easier to read if some sort of cutout was made.
Flog score and future change-based triage
Now that we have the score, we can triage it. There are other code metrics such as Cyclomatic Complexity and Perceived Complexity. Although whether or not they are completely consistent with actual complexity may have the room of improvement, I believe flog score.
It is an option to fix what is wrong locally on a method-by-method basis, but I personally think it is better to look at it on a class-by-class basis or on a concept-by-concept basis such as a combination of controller and model. It helps us understand what kind of domain handling is complicated.
I make a DA table with future changes and flog scores to determine.
Item A | Item B | Item C | |
---|---|---|---|
flog score | 1500 | 800 | 50 |
development plan | ○ | ◎ | △ |
I think it would be a good idea to refactor Item B first, which has a reasonably bad score and is planned to be developed.
By the way, in team development, it would be difficult to make major modifications to a part that is currently being actively changed. It would cause conflicts. The successful strategy is to go for areas that are a little more settled, have plans to resume development, and where the code is hard to read. That is another point.
Can CI prevent this in advance?
In other point of view, although many companies are doing it, it would be interesting if CI can show the difference from the default branch.
According to a Microsoft Research study I read earlier, Code Complexity was a higher indicator of Recall than Code Coverage as a factor in predicting bugs.
In my opinion, software bugs are similar to medical decisions. Recall is more important than Precision. When I say software bug, a bug refers to a rare phenomenon, and if a large percentage of the total code is okay, it may be similar to medical data.
While there are many companies that outputs whether or not the tests are passing in CI, there are not many companies that outputs Abc Size. "The pull request you made raised the code complexity score 10 points compared to the original value of existing files. 20 points higher than the repository average for new files added. " The warnings like this would raise the level of awareness of clean code.
Future Work
I would like to do something equivalent to flog since rubocop seems to be able to measure Cyclomatic Complexity and Perceived Complexity.