Skip to content

Commit 51ecf48

Browse files
committed
ruby: first draft qhelp plus some tweaks
1 parent f6cb090 commit 51ecf48

File tree

5 files changed

+72
-31
lines changed

5 files changed

+72
-31
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
When an AcriveRecord query is executed synchronously, the application is blocked until the query completes. This can lead to poor performance, especially when the query is slow or when many queries are executed in sequence. This query identifies ActiveRecord queries that could be executed asynchronously to improve performance.
9+
Specifically, this query identifies ActiveRecord queries that are executed in a loop. If each query is independent of the others, the queries could be executed in parallel by using the built-in Ruby <code>load_async</code> method.
10+
In those cases, where the query includes a <code>pluck</code> call, the query could be executed asynchronously by using the <code>async_pluck</code> method.
11+
</p>
12+
</overview>
13+
<recommendation>
14+
<p>If possible, split the loop into two. The first creates a map of promises resolving to the async query results. The second runs throuhg this map, waiting on each promise, and does whatever the original loop did with the result of the query.</p>
15+
</recommendation>
16+
<example>
17+
<p>The following (suboptimal) example code executes a series of ActiveRecord queries in a loop. The queries are independent of each other, so they could be executed in parallel to improve performance.</p>
18+
<sample src="examples/straight_loop.rb" />
19+
<p>To be able to fetch the necessary information asynchronously, we first pull it out into its own (implicit) loop:
20+
<sample src="examples/preload.rb" />
21+
<p>We can now use the <code>async_pluck</code> method to execute the queries in parallel.</p>
22+
<sample src="examples/async_pluck.rb" />
23+
</example>
24+
</qhelp>

ruby/ql/src/queries/performance/CouldBeAsync.ql

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,21 @@
11
/**
2-
* @id ruby/could-be-async
2+
* @name Could be async
3+
* @description Use `ActiveRecord::Relation#load_async` to load records asynchronously.
34
* @kind problem
45
* @problem.severity info
5-
* @problem.description Use `ActiveRecord::Relation#load_async` to load records asynchronously.
6+
* @precision high
7+
* @id rb/could-be-async
8+
* @tags performance
69
*/
710

811
import ruby
9-
// private import codeql.ruby.CFG
1012
import codeql.ruby.Concepts
1113
import codeql.ruby.frameworks.ActiveRecord
1214

13-
// collection.each { |item| expensiveCall(item) }
14-
predicate expensiveCall(DataFlow::CallNode c) { c instanceof SqlExecution }
15-
1615
string loopMethodName() {
17-
// TODO: check these
1816
result in [
1917
"each", "reverse_each", "map", "map!", "foreach", "flat_map", "in_batches", "one?", "all?",
2018
"collect", "collect!", "select", "select!", "reject", "reject!"
21-
// , "collect", "select", "reject", "find_all", "find",
22-
// "detect", "any?", "all?", "one?", "none?", "one", "none", "min", "max", "minmax", "min_by",
23-
// "max_by", "minmax_by", "sort", "sort_by", "sort_by!", "sort"
2419
]
2520
}
2621

@@ -51,38 +46,22 @@ predicate happensInInnermostLoop(LoopingCall loop, DataFlow::CallNode e) {
5146
not happensInOuterLoop(loop, e)
5247
}
5348

54-
// Active Record
55-
private class LoadElementsCall extends ActiveRecordInstanceMethodCall {
56-
LoadElementsCall() {
57-
this.getMethodName() in ["latest", "find_by", "where", "count", "find"] and
58-
not any(PluckCall p).chaines() = this
59-
}
60-
}
61-
6249
private class PluckCall extends ActiveRecordInstanceMethodCall {
6350
PluckCall() { this.getMethodName() in ["pluck"] }
6451

65-
ActiveRecordInstanceMethodCall chaines() { result = getChain(this.getInstance()) }
52+
ActiveRecordInstance chaines() { result = getChain(this) }
6653
}
6754

68-
private ActiveRecordInstanceMethodCall getChain(ActiveRecordInstanceMethodCall c) {
69-
result = c
55+
private ActiveRecordInstance getChain(ActiveRecordInstanceMethodCall c) {
56+
result = c.getInstance()
7057
or
7158
result = getChain(c.getInstance())
7259
}
7360

74-
ActiveRecordInstanceMethodCall longChain(ActiveRecordInstanceMethodCall c) {
75-
2 < strictcount(ActiveRecordInstanceMethodCall ch | ch = getChain(c)) and
76-
result = getChain(c) and
77-
result != c
78-
}
79-
80-
ActiveRecordInstanceMethodCall pluckChain(PluckCall c) { result = longChain(c) }
81-
82-
// from LoopingCall loopCall
83-
// select loopCall, loopCall.getLoopBlock()
8461
from LoopingCall loop, DataFlow::CallNode call, string message
8562
where
63+
not call.getLocation().getFile().getAbsolutePath().matches("%test%") and
64+
not call = any(PluckCall p).chaines() and
8665
happensInInnermostLoop(loop, call) and
8766
(
8867
call instanceof ActiveRecordModelFinderCall and
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
require 'async_pluck'
2+
3+
# Preload User data in parallel
4+
user_data = User.where(login: repo_names_by_owner.keys).async_pluck(:login, :id, :type).to_h do |login, id, type|
5+
[login, { id: id, type: type == "User" ? "USER" : "ORGANIZATION" }]
6+
end
7+
8+
repo_names_by_owner.each do |owner_slug, repo_names|
9+
owner_info = user_data[owner_slug]
10+
owner_id = owner_info[:id]
11+
owner_type = owner_info[:type]
12+
rel_conditions = { owner_id: owner_id, name: repo_names }
13+
14+
nwo_rel = nwo_rel.or(RepositorySecurityCenterConfig.where(rel_conditions)) unless neg
15+
nwo_rel = nwo_rel.and(RepositorySecurityCenterConfig.where.not(rel_conditions)) if neg
16+
end
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Preload User data
2+
user_data = User.where(login: repo_names_by_owner.keys).pluck(:login, :id, :type).to_h do |login, id, type|
3+
[login, { id: id, type: type == "User" ? "USER" : "ORGANIZATION" }]
4+
end
5+
6+
repo_names_by_owner.each do |owner_slug, repo_names|
7+
owner_info = user_data[owner_slug]
8+
owner_id = owner_info[:id]
9+
owner_type = owner_info[:type]
10+
rel_conditions = { owner_id: owner_id, name: repo_names }
11+
12+
nwo_rel = nwo_rel.or(RepositorySecurityCenterConfig.where(rel_conditions)) unless neg
13+
nwo_rel = nwo_rel.and(RepositorySecurityCenterConfig.where.not(rel_conditions)) if neg
14+
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
repo_names_by_owner.map do |owner_slug, repo_names|
2+
owner_id, owner_type = User.where(login: owner_slug).pluck(:id, :type).first
3+
owner_type = owner_type == "User" ? "USER" : "ORGANIZATION"
4+
rel_conditions = { owner_id: owner_id, name: repo_names }
5+
6+
nwo_rel = nwo_rel.or(RepositorySecurityCenterConfig.where(rel_conditions)) unless neg
7+
nwo_rel = nwo_rel.and(RepositorySecurityCenterConfig.where.not(rel_conditions)) if neg
8+
end

0 commit comments

Comments
 (0)