Slide 1

Slide 1 text

Cleaning up a huge ruby application RubyKaigi 2019 @shia

Slide 2

Slide 2 text

•Sangyong Sim(@shia) •Cookpad Inc. •Software Engineer •i18n team of w.r-l.o, elixirschool !2

Slide 3

Slide 3 text

!3

Slide 4

Slide 4 text

Index •about cookpad_all •define the code want to delete •what makes it difficult •what we try & solve !4

Slide 5

Slide 5 text

cookpad_all

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

cookpad_all !8 cookpad (web) pantry (api) kuroko (batch) papa (admin) mobile shared

Slide 9

Slide 9 text

cookpad_all •Make a bug to fix a bug •Gems are too old version which have no feature we want, and it's hard to update •A lot of monkey patch to Object, String, gems !9

Slide 10

Slide 10 text

Project Odaiba

Slide 11

Slide 11 text

Project Odaiba •Clean up codes •Let it work on (docker) container •Extract system/services which mixed in it !11

Slide 12

Slide 12 text

Project Odaiba •Clean up codes ‣ Today's topic •Let it work on (docker) container •Extract system/services which mixed in it !12

Slide 13

Slide 13 text

the code we want to delete

Slide 14

Slide 14 text

the code we may delete •Known unnecessary code •Unknown unnecessary code ‣ Executed meaningless code ‣ Not executed code !14

Slide 15

Slide 15 text

the code we may delete •Known unnecessary code •Unknown unnecessary code ‣ Executed meaningless code ‣ Not executed code !15

Slide 16

Slide 16 text

the code we may delete •Known unnecessary code •Unknown unnecessary code ‣ Executed meaningless code ‣ Not executed code !16

Slide 17

Slide 17 text

Why delete code?

Slide 18

Slide 18 text

why delete code? •Reduce code to understand •Reduce library dependency •Fast test, fast boot !18

Slide 19

Slide 19 text

why delete code? •Better productivity !19

Slide 20

Slide 20 text

What makes it difficult?

Slide 21

Slide 21 text

What makes it difficult? •High cost(especially time) •Low priority •Continuously growing !21

Slide 22

Slide 22 text

What makes it difficult? !22 if condition_seem_not_to_be_false # .. else foo_without_bar end

Slide 23

Slide 23 text

What makes it difficult? !23 def foo # ... end def foo_with_bar # ... end alias_method_chain :foo, :bar alias_method_chain :foo, :bar # => alias_method :foo_without_bar, :foo alias_method :foo, :foo_with_bar

Slide 24

Slide 24 text

What makes it difficult? •High cost(especially time) •Low priority •Continuously growing !24

Slide 25

Slide 25 text

What makes it difficult? •High cost(especially time) •Low priority •Continuously growing !25

Slide 26

Slide 26 text

What makes it difficult? !26

Slide 27

Slide 27 text

What makes it difficult? •High cost(especially time) •Low priority •Continuously growing !27

Slide 28

Slide 28 text

What we can do?

Slide 29

Slide 29 text

What can we do •Low priority ‣ Raise priority •Continuously growing •High cost(especially time) !29

Slide 30

Slide 30 text

What can we do •Low priority ‣ Raise priority •Continuously growing ‣ Continuously delete code •High cost(especially time) !30

Slide 31

Slide 31 text

What can we do •Low priority ‣ Raise priority •Continuously growing ‣ Continuously delete code •High cost(especially time) ‣ Auto detect potential unused code to reduce cost !31

Slide 32

Slide 32 text

What we try

Slide 33

Slide 33 text

Let continuously delete code

Slide 34

Slide 34 text

KitchenCleaner •Auto-detect potential unused code •Guess people related these code •Open issue or ping assignee •Give context as much as possible for decision making !34

Slide 35

Slide 35 text

KitchenCleaner !35

Slide 36

Slide 36 text

KitchenCleaner !36

Slide 37

Slide 37 text

KitchenCleaner !37

Slide 38

Slide 38 text

Target code •Controller which has no pv in a year •Batch class which have no execution history in 3 month •Unused (chanko) unit !38

Slide 39

Slide 39 text

Target developer •List up authors from "git log" •Filter out who quited the job •Random assign from it !39

Slide 40

Slide 40 text

Current status •Target code is reducing, but not fast •It leaves a good context !40

Slide 41

Slide 41 text

Logging code execution on production

Slide 42

Slide 42 text

!42 ko1: It seems to be possible to find out unused codes on production environment by iseq lazy loading introduced in Ruby 2.4 shia: Why not? Let's try! ko1: I wrote the patch on my way to work.

Slide 43

Slide 43 text

Ship it!!!!

Slide 44

Slide 44 text

IseqLogger •Logging when iseq first executed. !44

Slide 45

Slide 45 text

Wait, What is "iseq"?

Slide 46

Slide 46 text

InstructionSequence(iseq) •A compiled sequence of instructions for the Ruby Virtual Machine. !46

Slide 47

Slide 47 text

InstructionSequence(iseq) !47 # sample.rb class Cat def sleep? true end end

Slide 48

Slide 48 text

InstructionSequence(iseq) !48 > puts RubyVM::InstructionSequence.compile_file('sample.rb').disasm == disasm: #@sample.rb:1 (1,0)-(5,3)> (catch: FALSE) ... == disasm: #@sample.rb:1 (1,0)-(5,3)> (catch: FALSE) ... == disasm: # (catch: FALSE) ...

Slide 49

Slide 49 text

InstructionSequence(iseq) !49 - - sleep?

Slide 50

Slide 50 text

Logging code execution !50 # sample.rb class Cat # <- executed def sleep? # <- not executed true end def go_out # <- executed # ... end end Cat.new.go_out

Slide 51

Slide 51 text

Logging code execution !51 // https://github.com/ruby/ruby/blob/v2_4_3/vm_core.h#L415-L424 static inline const rb_iseq_t * rb_iseq_check(const rb_iseq_t *iseq) { #if USE_LAZY_LOAD if (iseq->body == NULL) { rb_iseq_complete((rb_iseq_t *)iseq); } #endif return iseq; }

Slide 52

Slide 52 text

Logging code execution !52 static inline const rb_iseq_t * rb_iseq_check(const rb_iseq_t *iseq) { @@ -419,6 +429,11 @@ rb_iseq_check(const rb_iseq_t *iseq) if (iseq->body == NULL) { rb_iseq_complete((rb_iseq_t *)iseq); } +#endif +#if USE_EXECUTED_CHECK + if ((iseq->flags & ISEQ_FL_EXECUTED) == 0) { + rb_iseq_executed_check_dump((rb_iseq_t *)iseq); + } #endif

Slide 53

Slide 53 text

Performance •Impact to performance is almost zero. •Boot time might be slower. ‣ In our case, logging via File I/O gives 2~3% slower boot time. !53

Slide 54

Slide 54 text

Transfer logs app fluentd Redshift •log to file when it triggered. •{ created_at:, filename:, lineno:, label: } !54

Slide 55

Slide 55 text

Generate report •What we need ‣ Which iseq is used or not ‣ Base on latest source code !55

Slide 56

Slide 56 text

Logging code execution !56 Iseq = Struct.new(:path, :lineno, :label) def traverse(results, iseq) results << Iseq.new(iseq.absolute_path.sub(PROJECT_BASE_PATH + '/', ''), iseq.first_lineno, iseq.label) iseq.each_child do |child| traverse(results, child) end results end iseq = RubyVM::InstructionSequence.compile_file(some_path)

Slide 57

Slide 57 text

Logging code execution !57 { created_at: ..., filename: 'sample.rb', lineno: 1, label: 'main' } { created_at: ..., filename: 'sample.rb', lineno: 1, label: '' } { created_at: ..., filename: 'sample.rb', lineno: 2, label: 'sleep?' } { created_at: ..., filename: 'sample.rb', lineno: 1, label: 'main' } { created_at: ..., filename: 'sample.rb', lineno: 1, label: '' } Executed iseqs Iseqs from latest code

Slide 58

Slide 58 text

Logging code execution !58

Slide 59

Slide 59 text

Logging code execution !59

Slide 60

Slide 60 text

Logging code execution !60

Slide 61

Slide 61 text

Logging code execution !61

Slide 62

Slide 62 text

Judge which log is usable •How to judge which log is usable or not? !62

Slide 63

Slide 63 text

Judge which log is usable •How to judge which log is usable or not? ‣ Let log usable if previous log have same filename + lineno + label with latest source code. !63

Slide 64

Slide 64 text

Judge which log is usable !64 # revision A # monthly_batch.rb # ... # recipe.rb class Recipe def author_name author.name end end # revision B # monthly_batch.rb # ... # recipe.rb class Recipe def author_name author.name end def author_id author.id end end

Slide 65

Slide 65 text

Judge which log is usable !65 Logs - 4/19, recipe.rb, L1, “”, revision A - 4/19, recipe.rb, L2, “”, revision A - 4/19, monthly_batch.rb, L1, “”, revision A - 4/20, recipe.rb, L1, “”, revision B - 4/20, recipe.rb, L6, “”, revision B # revision B # monthly_batch.rb # ... # recipe.rb class Recipe def author_name author.name end def author_id author.id end end

Slide 66

Slide 66 text

Judge which log is usable •In our case, only 65% of files updated in a year •Assume file which rarely updated might have unused codes then frequently updated one !66

Slide 67

Slide 67 text

Good point •We could find out which template rendered or not!! !67

Slide 68

Slide 68 text

Bad point •Need to build Ruby •Using iseq for detecting could miss some cases we want !68

Slide 69

Slide 69 text

Bad point !69 # It has dead condition if some_method_returns_always_false not_called_in_if else always_called end # What we want always_called not_called_in_if

Slide 70

Slide 70 text

Bad point !70 # It has dead condition if some_method_returns_always_false not_called_in_if else always_called end not_called_in_if # What we want always_called not_called_in_if

Slide 71

Slide 71 text

Bad point •Need to build Ruby •Using iseq for detecting could miss some cases we want !71

Slide 72

Slide 72 text

Logging code execution on production per line!!

Slide 73

Slide 73 text

!73 mame: I think coverage is enough for that use case. shia: I'm worry about degrade performance by coverage, because it tries to hook when every line execution. mame: How about remove hook after it triggered once?

Slide 74

Slide 74 text

Ship it!!!!

Slide 75

Slide 75 text

First of all...

Slide 76

Slide 76 text

Let cookpad_all to use Ruby 2.6!!!!!!

Slide 77

Slide 77 text

Oneshot coverage •It just work as same as normal coverage, but it only check line executed or not. ‣ It removes hook after triggered. !77

Slide 78

Slide 78 text

Oneshot coverage !78 require "coverage" Coverage.start(oneshot_lines: true) load "test.rb" p Coverage.result # {"test.rb"=>{:oneshot_lines=>[2, 10, 3, 4, 11]}} # 1: # test.rb # 2: def foo(n) # 3: if n <= 10 # 4: p "n < 10" # 5: else # 6: p "n >= 10" # 7: end # 8: end # 9: # 10: foo(1) # 11: foo(2)

Slide 79

Slide 79 text

Transfer logs app DynamoDB Redshift •Send log when: ‣ per 1 hour ‣ after batch execution finished •{ filename:, md5_hash:, executed_lines: } !79 per 1 hour per 1 day

Slide 80

Slide 80 text

Transfer logs app DynamoDB Redshift •Use dynamoDB: ‣ Sending logs per line is not practical approach in this case ‣ Send data to Redshift as daily snapshot •{ filename:, md5_hash:, executed_lines: } !80 per 1 hour per 1 day Purge per day

Slide 81

Slide 81 text

How to merge logs !81 Case: need to merge - DynamoDB => "recipe.rb-somehash": [1,2,3,5,9] - Coverage.result => "recipe.rb-somehash": [1,2,3,6,7,8] - New data => "recipe.rb-somehash": [1,2,3,5,9,6,7,8] Case: Don't need to merge - DynamoDB => "recipe.rb-somehash": [1,2,3,5,9] - Coverage.result => "recipe.rb-somehash": [1,2,3]

Slide 82

Slide 82 text

Transfer logs app DynamoDB Redshift •Use dynamoDB: ‣ Sending logs per line is not practical approach in this case ‣ Send data to Redshift as daily snapshot •{ filename:, md5_hash:, executed_lines: } !82 per 1 hour per 1 day Purge per day

Slide 83

Slide 83 text

Generate report •What we need ‣ Which line is used or not ‣ Base on latest source code !83

Slide 84

Slide 84 text

Generate report !84

Slide 85

Slide 85 text

Judge which log is usable •How to judge which log is usable or not? !85

Slide 86

Slide 86 text

Judge which log is usable •In our case, only 65% of files updated in a year •Assume file which rarely updated might have unused codes then frequently updated one !86

Slide 87

Slide 87 text

Judge which log is usable •Use md5 hash whether log is usable or not !87

Slide 88

Slide 88 text

Good point •No need to build Ruby! •Per line logs give us much more information !88

Slide 89

Slide 89 text

Bad point •Too many number of logs •Hard to find unused file (than IseqLogger) !89

Slide 90

Slide 90 text

Bad point !90 1: class Recipe 2: def name 3: @name 4: end 5: 6: def author 7: @author 8: end 9:end

Slide 91

Slide 91 text

Bad point !91 1: class Recipe 2: def name 3: @name -: end -: 6: def author 7: @author -: end -:end

Slide 92

Slide 92 text

Bad point !92 # oneshot coverage 1: class Recipe 2: def name 3: @name -: end -: 6: def author 7: @author -: end -:end # IseqLogger class Recipe # <- Executed def name # <- Not executed @name end def author # <- Not executed @author end end

Slide 93

Slide 93 text

Current status •Running on production !93

Slide 94

Slide 94 text

Performance Impact !94

Slide 95

Slide 95 text

Performance Impact !95

Slide 96

Slide 96 text

Coverband •https://github.com/danmayer/coverband/ •We don't use this: ‣ oneshot coverage is not supported, yet ‣ verbose toolkit to us ‣ might loss unused information with not changed file !96

Slide 97

Slide 97 text

oneshot_coverage •https://github.com/riseshia/oneshot_coverage •Provide minimum toolkit for logging •Support (only) oneshot coverage mode •Focus on how to emit data from app !97

Slide 98

Slide 98 text

Auto-detect unused codes on production is not almighty.

Slide 99

Slide 99 text

Rome was not built in a day

Slide 100

Slide 100 text

!100

Slide 101

Slide 101 text

!101

Slide 102

Slide 102 text

!102

Slide 103

Slide 103 text

This is everyone's achievement.

Slide 104

Slide 104 text

Summary

Slide 105

Slide 105 text

!105 •Low priority ‣ Let code cleaning be a part of project. •Continuously growing ‣ Let developers remove their own unused codes per month •High cost(especially time) ‣ Auto detect potential unused code on production to reduce cost Summary

Slide 106

Slide 106 text

Conclusion !106

Slide 107

Slide 107 text

It is necessary to work hard for cleaning codes !107

Slide 108

Slide 108 text

It is necessary to work hard for cleaning codes !108 It is necessary to work little more