Quantcast
Channel: プログラミング
Viewing all articles
Browse latest Browse all 8275

How Flog can be used to visualize need for refactoring and persuade boss to return debt - algonote(en)

$
0
0

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.


Viewing all articles
Browse latest Browse all 8275

Trending Articles