by Renee French. (http://reneefrench.blogspot.com/) The design is licensed under the Creative Commons 4.0 Attributions license. Read this article for more details: https://blog.golang.org/gopher
◦ https://github.com/sue445/rubicure (since 2013, 10th Anniversary!) • Maintainer of Itamae, rspec-parameterised and more more gems 9 Gems I Implemented
for cop are limitless • Detecting N+1 queries is easy • Limited patterns, but auto-correct of N+1 queries is possible • ISUCON is Mixed Martial Arts 18 Conclusion
I made for ISUCON • Monitor Sinatra app with Datadog • Implement https://github.com/tkuchiki/alp with fluentd and Datadog dashboard • Ruby 3.2-dev YJIT vs Ruby 3.1 YJIT See my articles (ja) • https://sue445.hatenablog.com/entry/2022/07/04/084915 • https://sue445.hatenablog.com/entry/2022/07/24/190412 20 What I will NOT talk about in this talk
Japan. (since 2012) ◦ https://isucon.net/ ◦ ISUCON is a trademark or registered trademark of LINE Corporation. • Multiple reference implementations are provided for applications ◦ e.g. Go, Node.js, Perl, PHP, Python, Ruby, Rust 22 About ISUCON
application ◦ Add index to table ◦ Add cache ◦ Upgrade pre-installed middlewares, tuning configuration ◦ Use ruby-head ▪ ruby-head is the fastest Ruby in the world! 25 ISUCON is Mixed Martial Arts
Development period: 2021/8 (ISUCON 11) - 2022/7 (ISUCON 12) • Features ◦ Isucon/Sinatra department ◦ Isucon/Mysql2 department ◦ Isucon/Mysql2/NPlusOneQuery cop ▪ Checks that there’s no N+1 query ▪ Main topic of this talk 28 https://github.com/sue445/rubocop-isuco n
use RuboCop on a regular basis and am familiar with its use • I have created a custom cop and tool for RuboCop in the past ◦ https://github.com/sue445/rubocop-itamae ◦ https://github.com/sue445/rubocop_auto_corrector 30 Q. Why RuboCop?
RuboCop can detect violations and automatically correct them if possible ◦ In some cases, the machine may not be able to automatically correct the violation, so I only want to detect violations at that time ◦ e.g. nginx config, database index 32 vs Synvert
:html File.read(File.join(__dir__, '..', 'public', 'index.html')) end end 33 e.g. Isucon/Sinatra/ServeStaticFile https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Sinatra/ServeStaticFile.html
Isucon/Sinatra/ServeStaticFile: Serve static files on front server (e.g. nginx) instead of sinatra app content_type :html File.read(File.join(__dir__, '..', 'public', 'index.html')) end end 34 e.g. Isucon/Sinatra/ServeStaticFile https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Sinatra/ServeStaticFile.html
articles WHERE user_id = ?', user_id) ^^^^^^^^^^^ Isucon/Mysql2/WhereWithoutIndex: This where clause doesn't seem to have an index. (e.g. `ALTER TABLE articles ADD INDEX index_user_id (user_id)`) 37 e.g. Isucon/Mysql2/WhereWithoutIndex https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Mysql2/WhereWithoutIndex.html
https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Mysql2/WhereWithoutIndex.html # good (user_id is indexed) db.xquery('SELECT id, title FROM articles WHERE used_id = ?', user_id)
articles JOIN users ON users.id = articles.user_id') ^^^^^^^^^^^^^^^^ Isucon/Mysql2/JoinWithoutIndex: This join clause doesn't seem to have an index. (e.g. `ALTER TABLE articles ADD INDEX index_user_id (user_id)`) # good (user_id is indexed) db.xquery('SELECT id, title FROM articles JOIN users ON users.id = articles.user_id') 39 e.g. Isucon/Mysql2/JoinWithoutIndex https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Mysql2/JoinWithoutIndex.html
columns and assigns them to Ruby objects • Only necessary columns should be passed to the SELECT clause • All columns are output to the SELECT clause to make it easier to remove unnecessary columns • • 41 e.g. Isucon/Mysql2/SelectAsterisk https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Mysql2/SelectAsterisk.html # bad db.xquery('SELECT * FROM users') # good (Auto-corrected!) db.xquery('SELECT id, name FROM users')
◦ https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/ru bocop/cop/performance/collection_literal_in_loop.rb • Can be detected by AST if in the same method, but not if separate methods are used… 44 Detect SQL N+1 queries
a few others but could not get the expected results ◦ In particular, these gems could not parse isucon10-final's complex SQL ◦ https://github.com/isucon/isucon10-final/blob/e858b25/webapp/ruby /app.rb#L250-L318 ▪ a.k.a. Tourist Attractions 46 Parse SQL string and get as SQL AST
is removed in rubocop-isucon • Deleting would change the location information, so care is taken to ensure that the number of characters does not change ◦ https://github.com/sue445/rubocop-isucon/blob/v0.2.0/lib/rubocop/is ucon/gda.rb#L19-L25 48 Tips # Before db.xquery('SELECT id, title FROM `articles` WHERE user_id = ?', user_id) # After db.xquery('SELECT id, title FROM articles WHERE user_id = 0', user_id)
information • Cannot display offences in RuboCop or rewrite any node of SQL 51 Drawbacks of GDA gem (libgda) db.xquery(<<~SQL, jia_user_id) SELECT * FROM `isu` WHERE `jia_user_id` = ? ORDER BY `id` DESC ^^^^^^^^^^^^^^^^^ Isucon/Mysql2/WhereWithoutIndex: This where clause doesn't seem to have an index. (e.g. 'ALTER TABLE `isu` ADD INDEX `index_jia_user_id` (jia_user_id)') SQL
def visit_GDA_Nodes_Operation(node) return super unless node.operator pattern = operand_pattern(node) return super unless pattern node.location = search_operation_location(pattern) super end https://github.com/sue445/rubocop-isucon/blob/v0.2.0/lib/rubocop/isucon/gda/node_patcher.rb#L19-L29
creates a new instance • Even if I embed my own instance variable(location) in the Node class in the Visitor, I will not be able to retrieve it the next time you call the same method 57 Why????
But there are 30+ such AST methods • AST methods are methods in the C implementation, so prepend could not be used 58 Solution: memoization, but… def where_cond @where_cond ||= original_where_cond end
"#{method_name}_with_cache" do if (ret = instance_variable_get("@#{method_name}_with_cache")) ret else ret = send("#{method_name}_without_cache") instance_variable_set("@#{method_name}_with_cache", ret) ret end end alias_method "#{method_name}_without_cache", method_name alias_method method_name, "#{method_name}_with_cache" end
Basically, just add Parser::Source::Range (Ruby AST location information) and RuboCop::Isucon::GDA::NodeLocation (SQL AST location information) • However, Ruby is very expressive even when writing a single string, so it was necessary to consider many different cases. 70 Bringing SQL AST into the Ruby World
it is easy to just detect offenses • However, it is very difficult to deal with auto correct of offenses • It took me about 2 months to figure out what kind of SQL N+1 query could be automatically modified by RuboCop • It took me 1 month to come up with the idea and actually implement it 79 Find N+1 query patterns that can be mechanically modified
a function called includes to avoid N+1 and issue a query when an association between models is defined by has_many or belongs_to • ISUCON's reference implementation doesn’t use ActiveRecord, but plain SQL. • However, I looked at the plain SQL and thought that an association-like SQL and table structure could improve the N+1 query by automatically modifying the SQL in the same way 80 How did I come up with the idea?
end Excerpt from Ruby on Rails Guides SELECT books.* FROM books LIMIT 10 SELECT authors.* FROM authors WHERE authors.book_id = 1 SELECT authors.* FROM authors WHERE authors.book_id = 2 : : 11 queries
end Excerpt from Ruby on Rails Guides SELECT books.* FROM books LIMIT 10 SELECT authors.* FROM authors WHERE authors.book_id IN (1,2,3,4,5,6,7,8,9,10) 2 queries
(Does not include UNION, JOIN, or subqueries) • GROUP BY clause doesn’t exist • Aggregate functions (e.g. COUNT, MAX, MIN, SUM, AVG) doesn’t exist in the SELECT clause • Only one condition in the WHERE clause and the column in which it appears is either a Primary Key or a single column Unique Key 84 Conditions for safe automatic correction
replace_to_2_lines end end end RuboCop::Cop::Isucon::Correctors::Mysql2NPlusOneQueryCorrector::ReplaceMethods https://github.com/sue445/rubocop-isucon/blob/v0.2.0/lib/rubocop/cop/isucon/correctors/n_plus_one_query_corrector/re place_methods.rb#L10-L15
} users = db.xquery('SELECT * FROM `users` WHERE `id` IN (?)', user_ids) users_by_id = users.each_with_object({}) do |user, hash| hash[user[:id]] = user end courses.map do |course| teacher = users_by_id[course[:teacher_id]] end
} users = db.xquery('SELECT * FROM `users` WHERE `id` IN (?)', user_ids) users_by_id = users.each_with_object({}) do |user, hash| hash[user[:id]] = user end courses.map do |course| teacher = users_by_id[course[:teacher_id]] end Convert WHERE `id` = ? inside the loop to WHERE `id` IN (?) outside the loop to avoid N+1 queries
} users = db.xquery('SELECT * FROM `users` WHERE `id` IN (?)', user_ids) users_by_id = users.each_with_object({}) do |user, hash| hash[user[:id]] = user end courses.map do |course| teacher = users_by_id[course[:teacher_id]] end Create a Hash with user_id as key and user as value
FROM `users` WHERE `id` IN (?)', courses.map { |course| course[:teacher_id] }).each_with_object({}) { |v, hash| hash[v[:id]] = v } teacher = @users_by_id[course[:teacher_id]] end
into multiple lines in RuboCop ◦ Because it is necessary to consider indentation • I'm trying to reduce the number of lines as much as possible, so I'm using one-liners to push method chains in and out to get more lines • However, it was difficult to understand this case if it was completely on one line, so I made it on two lines as a bitter effort 93 Tips
corrected automatically with this method • isucon12 qualify’s theme was Multi-tenant SaaS app ◦ https://isucon.net/archives/56850281.html (ja) ◦ https://github.com/isucon/isucon12-qualify ◦ MySQL + SQLite!!! • ISUCON is a mixed martial art, so just being able to do static analysis and automatic modification of N+1 queries is not enough to win 96 Result
for cop are limitless • Detecting N+1 queries is easy • Limited patterns, but auto-correct of N+1 queries is possible • ISUCON is Mixed Martial Arts 97 Conclusion
< Sinatra::Base enable :logging end # bad class App < Sinatra::Base end # good class App < Sinatra::Base disable :logging end https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Sinatra/DisableLogging.html
`printf "%s" #{Shellwords.shellescape(src)} | openssl dgst -sha512 | sed 's/^.*= //'`.strip end # good def digest(src) OpenSSL::Digest::SHA512.hexdigest(src) end https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Shell/Backtick.html