shiba 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/LICENSE +13 -0
- data/Rakefile +13 -5
- data/bin/explain +22 -17
- data/lib/shiba/analyzer.rb +17 -11
- data/lib/shiba/configure.rb +4 -0
- data/lib/shiba/explain/postgres_explain.rb +3 -3
- data/lib/shiba/output.rb +14 -9
- data/lib/shiba/{explain → parsers}/postgres_explain_index_conditions.rb +7 -20
- data/lib/shiba/version.rb +1 -1
- data/web/babel.config.js +5 -0
- data/web/package-lock.json +9299 -2235
- data/web/package.json +44 -14
- data/web/src/App.vue +185 -0
- data/web/src/assets/logo.png +0 -0
- data/web/src/components/Backtrace.vue +36 -0
- data/web/src/components/HelloWorld.vue +58 -0
- data/web/src/components/Message.js +104 -0
- data/web/src/components/Message.vue +106 -0
- data/web/src/components/Query.vue +112 -0
- data/web/src/components/Sql.vue +22 -0
- data/web/src/main.js +8 -0
- data/web/src/query_data.js +47 -0
- data/web/vue.config.js +11 -0
- metadata +17 -12
- data/web/bootstrap.min.css +0 -7
- data/web/dist/bundle.js +0 -189
- data/web/main.css +0 -70
- data/web/main.js +0 -6
- data/web/results.html.erb +0 -412
- data/web/vue.js +0 -11055
- data/web/webpack.config.js +0 -14
data/web/main.css
DELETED
@@ -1,70 +0,0 @@
|
|
1
|
-
|
2
|
-
.query-info-box {
|
3
|
-
border: 1px solid black;
|
4
|
-
padding: 10px;
|
5
|
-
margin: 5px;
|
6
|
-
}
|
7
|
-
|
8
|
-
.backtrace {
|
9
|
-
font-family: monospace;
|
10
|
-
background-color: #EEEEEE;
|
11
|
-
padding: 5px;
|
12
|
-
margin: 10px
|
13
|
-
}
|
14
|
-
|
15
|
-
.sql {
|
16
|
-
font-family: monospace;
|
17
|
-
}
|
18
|
-
|
19
|
-
.badge {
|
20
|
-
color: black;
|
21
|
-
background-color: white;
|
22
|
-
border-style: solid;
|
23
|
-
border-width: 2px;
|
24
|
-
margin-right: 5px;
|
25
|
-
width: 100px;
|
26
|
-
}
|
27
|
-
|
28
|
-
.shiba-badge-info {
|
29
|
-
border-color: #78b0ec;
|
30
|
-
}
|
31
|
-
|
32
|
-
.shiba-badge-danger {
|
33
|
-
border-color: #ff655d;
|
34
|
-
}
|
35
|
-
|
36
|
-
.shiba-badge-success {
|
37
|
-
border-color: green;
|
38
|
-
}
|
39
|
-
|
40
|
-
.shiba-badge-warning {
|
41
|
-
border-color: #ffb100;
|
42
|
-
}
|
43
|
-
|
44
|
-
.shiba-badge-td {
|
45
|
-
width: 100px;
|
46
|
-
}
|
47
|
-
|
48
|
-
.shiba-messages {
|
49
|
-
margin: 0px;
|
50
|
-
margin-top: 10px;
|
51
|
-
width: 100%;
|
52
|
-
}
|
53
|
-
|
54
|
-
.shiba-messages td {
|
55
|
-
padding-top: 5px;
|
56
|
-
}
|
57
|
-
|
58
|
-
.shiba-message {
|
59
|
-
padding-right: 10px;
|
60
|
-
width: 90%;
|
61
|
-
}
|
62
|
-
|
63
|
-
.running-totals {
|
64
|
-
align: right;
|
65
|
-
font-family: monospace;
|
66
|
-
}
|
67
|
-
|
68
|
-
|
69
|
-
[v-cloak] { display: none }
|
70
|
-
|
data/web/main.js
DELETED
data/web/results.html.erb
DELETED
@@ -1,412 +0,0 @@
|
|
1
|
-
<html>
|
2
|
-
<head>
|
3
|
-
<title>Shiba results for <%= Time.now %></title>
|
4
|
-
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet">
|
5
|
-
</head>
|
6
|
-
<body>
|
7
|
-
<% data[:js].each do |js| %>
|
8
|
-
<% if ENV['SHIBA_DEBUG'] %>
|
9
|
-
<script type="text/javascript" src="file://<%= js %>"></script>
|
10
|
-
<% else %>
|
11
|
-
<script type="text/javascript">
|
12
|
-
<%= File.read(js) %>
|
13
|
-
</script>
|
14
|
-
<% end %>
|
15
|
-
<% end %>
|
16
|
-
|
17
|
-
<% data[:css].each do |css| %>
|
18
|
-
<style type="text/css">
|
19
|
-
<%= File.read(css) %>
|
20
|
-
</style>
|
21
|
-
<% end %>
|
22
|
-
|
23
|
-
<script language="javascript">
|
24
|
-
var data = <%= data.to_json %>;
|
25
|
-
var queriesByTable = [];
|
26
|
-
var queriesByTableLow = [];
|
27
|
-
var queriesHaveFuzzed = false;
|
28
|
-
var severityIndexes = { high: 1, medium: 2, low: 3, none: 4 };
|
29
|
-
|
30
|
-
function sortByFunc(fields) {
|
31
|
-
return function(a, b) {
|
32
|
-
for ( var i = 0 ; i < fields.length; i++ ) {
|
33
|
-
if ( a[fields[i]] < b[fields[i]] )
|
34
|
-
return -1;
|
35
|
-
else if ( a[fields[i]] > b[fields[i]] )
|
36
|
-
return 1;
|
37
|
-
}
|
38
|
-
return 0;
|
39
|
-
}
|
40
|
-
}
|
41
|
-
|
42
|
-
function Query(obj) {
|
43
|
-
Object.assign(this, obj);
|
44
|
-
this.severityIndex = severityIndexes[this.severity];
|
45
|
-
this.splitSQL();
|
46
|
-
this.makeSearchString();
|
47
|
-
};
|
48
|
-
|
49
|
-
Query.prototype = {
|
50
|
-
makeSearchString: function() {
|
51
|
-
var arr = [this.sql];
|
52
|
-
arr = arr.concat(this.messages.map(function(m) { return m.tag }).join(':'));
|
53
|
-
arr = arr.concat(this.backtrace.join(':'));
|
54
|
-
|
55
|
-
this.searchString = arr.join(':').toLowerCase();
|
56
|
-
},
|
57
|
-
hasTag: function(tag) {
|
58
|
-
return this.messages.find(function(m) {
|
59
|
-
return m.tag == tag;
|
60
|
-
});
|
61
|
-
},
|
62
|
-
splitSQL: function() {
|
63
|
-
this.sqlFragments = this.sql.match(/(SELECT\s)(.*?)(\s+FROM .*)/i);
|
64
|
-
},
|
65
|
-
select: function () {
|
66
|
-
return this.sqlFragments[1];
|
67
|
-
},
|
68
|
-
fields: function () {
|
69
|
-
return this.sqlFragments[2];
|
70
|
-
},
|
71
|
-
rest: function(index) {
|
72
|
-
return this.sqlFragments.slice(index).join('');
|
73
|
-
}
|
74
|
-
};
|
75
|
-
|
76
|
-
data.queries.forEach(function(query) {
|
77
|
-
var q = new Query(query);
|
78
|
-
|
79
|
-
if ( q.severity == "none" ) {
|
80
|
-
queriesByTableLow.push(q);
|
81
|
-
} else {
|
82
|
-
queriesByTable.push(q);
|
83
|
-
}
|
84
|
-
|
85
|
-
if ( q.hasTag("fuzzed_data") )
|
86
|
-
queriesHaveFuzzed = true;
|
87
|
-
|
88
|
-
q.expandSelect = false;
|
89
|
-
|
90
|
-
var rCost = 0;
|
91
|
-
q.messages.forEach(function(m) {
|
92
|
-
if ( m.cost && m.cost != 0) {
|
93
|
-
rCost += m.cost;
|
94
|
-
m.running_cost = rCost;
|
95
|
-
} else {
|
96
|
-
m.running_cost = undefined;
|
97
|
-
}
|
98
|
-
});
|
99
|
-
});
|
100
|
-
|
101
|
-
var f = sortByFunc(['severityIndex', 'table']);
|
102
|
-
queriesByTable = queriesByTable.sort(f);
|
103
|
-
queriesByTableLow = queriesByTableLow.sort(f);
|
104
|
-
</script>
|
105
|
-
|
106
|
-
<script type="text/x-template" id="query-template">
|
107
|
-
<div class="query">
|
108
|
-
<div class="row">
|
109
|
-
<div class="col-3">
|
110
|
-
<a href="#" v-on:click="expandToggle">
|
111
|
-
<span stlye="text-align: right">{{ expandText }}</span>
|
112
|
-
</a>
|
113
|
-
{{ query.table }}
|
114
|
-
</div>
|
115
|
-
<div class="col-5">{{ truncate(query.sql, 50) }}</div>
|
116
|
-
<div class="col-3" v-html="makeURL(query.backtrace[0], shortLocation(query))"></div>
|
117
|
-
<div class="col-1">{{ query.severity }}</div>
|
118
|
-
</div>
|
119
|
-
<div class="row" v-if="expanded">
|
120
|
-
<div class="col-12">
|
121
|
-
<div class="query-info-box">
|
122
|
-
<query-sql v-bind:query="query"></query-sql>
|
123
|
-
<div v-if="query.backtrace && query.backtrace.length > 0">
|
124
|
-
Stack Trace:<br>
|
125
|
-
<div class="backtrace">
|
126
|
-
<div v-for="backtrace in query.backtrace" v-html="makeURL(backtrace, backtrace)"></div>
|
127
|
-
</div>
|
128
|
-
</div>
|
129
|
-
<table class="shiba-messages">
|
130
|
-
<component v-for="message in query.messages" v-bind:is="'tag-' + message.tag" v-bind="message"></component>
|
131
|
-
</table>
|
132
|
-
<% if ENV['SHIBA_DEBUG'] %>
|
133
|
-
<div style="font-size: 10px">md5: {{ query.md5 }}</div>
|
134
|
-
<% end %>
|
135
|
-
<div v-if="!rawExpanded">
|
136
|
-
<a href="#" v-on:click.prevent="rawExpanded = !rawExpanded">See full EXPLAIN</a>
|
137
|
-
</div>
|
138
|
-
<div v-else>
|
139
|
-
<a href="#" v-on:click.prevent="rawExpanded = !rawExpanded">hide EXPLAIN</a>
|
140
|
-
<pre class="backtrace">{{ JSON.stringify(query.raw_explain, null, 2) }}</pre>
|
141
|
-
</div>
|
142
|
-
</div>
|
143
|
-
</div>
|
144
|
-
</div>
|
145
|
-
</div>
|
146
|
-
</script>
|
147
|
-
|
148
|
-
<script>
|
149
|
-
var greenToRedGradient = [
|
150
|
-
'#57bb8a','#63b682', '#73b87e', '#84bb7b', '#94bd77', '#a4c073', '#b0be6e',
|
151
|
-
'#c4c56d', '#d4c86a', '#e2c965', '#f5ce62', '#f3c563', '#e9b861', '#e6ad61',
|
152
|
-
'#ecac67', '#e9a268', '#e79a69', '#e5926b', '#e2886c', '#e0816d', '#dd776e'];
|
153
|
-
|
154
|
-
var templateComputedFunctions = {
|
155
|
-
key_parts: function() {
|
156
|
-
if ( this.index_used && this.index_used.length > 0 )
|
157
|
-
return this.index_used.join(',');
|
158
|
-
else
|
159
|
-
return "";
|
160
|
-
},
|
161
|
-
fuzz_table_sizes: function() {
|
162
|
-
var h = {};
|
163
|
-
var tables = this.tables;
|
164
|
-
|
165
|
-
Object.keys(tables).forEach(function(k) {
|
166
|
-
console.log(k);
|
167
|
-
var size = tables[k];
|
168
|
-
if ( !h[size] )
|
169
|
-
h[size] = [];
|
170
|
-
|
171
|
-
h[size].push(k);
|
172
|
-
});
|
173
|
-
|
174
|
-
var sizesDesc = Object.keys(h).sort(function(a, b) { return b - a });
|
175
|
-
var str = "";
|
176
|
-
|
177
|
-
sizesDesc.forEach(function(size) {
|
178
|
-
str = str + h[size].join(", ") + ": " + size.toLocaleString() + " rows. ";
|
179
|
-
});
|
180
|
-
|
181
|
-
return str;
|
182
|
-
},
|
183
|
-
formatted_cost: function() {
|
184
|
-
var readPercentage = (this.rows_read / this.table_size) * 100.0;
|
185
|
-
if ( this.rows_read > 100 && readPercentage > 1 ) // todo: make better
|
186
|
-
return `${readPercentage.toFixed()}% (${this.rows_read.toLocaleString()}) of the`;
|
187
|
-
else
|
188
|
-
return this.rows_read.toLocaleString();
|
189
|
-
},
|
190
|
-
costToColor: function() {
|
191
|
-
var goodColor = [34, 160, 60];
|
192
|
-
var endColor = [255, 0, 0];
|
193
|
-
var costScale = this.cost ? this.cost / 0.5 : 0;
|
194
|
-
|
195
|
-
if ( costScale > 1 )
|
196
|
-
costScale = 1;
|
197
|
-
|
198
|
-
var pos = (costScale * (greenToRedGradient.length - 1)).toFixed();
|
199
|
-
|
200
|
-
return "border-color: " + greenToRedGradient[pos];
|
201
|
-
},
|
202
|
-
formattedRunningCost: function() {
|
203
|
-
if ( this.running_cost === undefined )
|
204
|
-
return "-";
|
205
|
-
else if ( this.running_cost < 1.0 )
|
206
|
-
return (this.running_cost * 100).toFixed() + "ms";
|
207
|
-
else
|
208
|
-
return this.running_cost.toFixed(1) + "s";
|
209
|
-
},
|
210
|
-
formatted_result: function() {
|
211
|
-
var rb = this.result_bytes;
|
212
|
-
var result;
|
213
|
-
if ( rb == 0 )
|
214
|
-
return "" + this.result_size + " rows";
|
215
|
-
else if ( rb < 1000 )
|
216
|
-
result = rb + " bytes ";
|
217
|
-
else if ( rb < 1000000 )
|
218
|
-
result = (rb / 1000).toFixed() + "kb ";
|
219
|
-
else
|
220
|
-
result = (rb / 1000000 ).toFixed(1) + "mb ";
|
221
|
-
|
222
|
-
return result + " (" + this.result_size.toLocaleString() + " rows)";
|
223
|
-
}
|
224
|
-
}
|
225
|
-
</script>
|
226
|
-
<% data[:tags].each do |tag, h| %>
|
227
|
-
<script type="text/x-template" id="tag-<%= tag %>-template">
|
228
|
-
<tr>
|
229
|
-
<td class="shiba-badge-td">
|
230
|
-
<a class="badge" v-bind:style="costToColor"><%= h['title'] %></a>
|
231
|
-
</td>
|
232
|
-
<td class="shiba-message">
|
233
|
-
<%= h['summary'] %>
|
234
|
-
</td>
|
235
|
-
<td class="running-totals">
|
236
|
-
{{ formattedRunningCost }}
|
237
|
-
</td>
|
238
|
-
</tr>
|
239
|
-
</script>
|
240
|
-
<script>
|
241
|
-
Vue.component('tag-<%= tag %>', {
|
242
|
-
template: '#tag-<%= tag %>-template',
|
243
|
-
props: [ 'table_size', 'result_size', 'table', 'cost', 'index', 'join_to', 'index_used', 'running_cost', 'tables', 'rows_read', 'result_bytes' ],
|
244
|
-
computed: templateComputedFunctions,
|
245
|
-
data: function () {
|
246
|
-
return { lastRunnningCost: undefined };
|
247
|
-
}
|
248
|
-
});
|
249
|
-
</script>
|
250
|
-
<% end %>
|
251
|
-
|
252
|
-
|
253
|
-
<script type="text/x-template" id="sql-template">
|
254
|
-
<div class="sql">
|
255
|
-
<span>{{ query.select() }}</span>
|
256
|
-
<span v-if="!expandFields && query.fields().length > 80">
|
257
|
-
<a href="#" v-on:click.prevent="expandFields = !expandFields">...</a>
|
258
|
-
</span>
|
259
|
-
<span v-else>{{ query.fields() }}</span>
|
260
|
-
<span>{{ query.rest(3) }}</span>
|
261
|
-
</div>
|
262
|
-
</script>
|
263
|
-
|
264
|
-
<script>
|
265
|
-
Vue.component('query-sql', {
|
266
|
-
template: '#sql-template',
|
267
|
-
props: ['query'],
|
268
|
-
data: function () {
|
269
|
-
return { expandFields: false }
|
270
|
-
}
|
271
|
-
});
|
272
|
-
</script>
|
273
|
-
|
274
|
-
<div id="app" v-cloak>
|
275
|
-
<v-dialog :width="600"></v-dialog>
|
276
|
-
<div class="container" style="">
|
277
|
-
<div class="row" v-if="hasFuzzed">
|
278
|
-
<div class="alert alert-warning" role="alert">
|
279
|
-
This query analysis was generated using estimated table sizes.
|
280
|
-
To improve these results and find other problem queries beyond missing indexes, we'll need more stats.<br/>
|
281
|
-
<a target="_blank" href="https://github.com/burrito-brothers/shiba/blob/master/README.md#going-beyond-table-scans">Find out how to get a more accurate analysis by feeding Shiba index stats</a>
|
282
|
-
</div>
|
283
|
-
</div>
|
284
|
-
<div class="row">
|
285
|
-
<div class="col-10"></div>
|
286
|
-
<div class="col-2"><input :value="search" @input="updateSearch" placeholder="search..."></div>
|
287
|
-
</div>
|
288
|
-
<div class="row">
|
289
|
-
<div class="col-12">We found {{ queries.length }} queries that
|
290
|
-
<span v-if="search == ''">deserve your attention:</span>
|
291
|
-
<span v-else=>match your search term</span>
|
292
|
-
</div>
|
293
|
-
</div>
|
294
|
-
<div class="row">
|
295
|
-
<div class="col-3">Table</div>
|
296
|
-
<div class="col-5">Query</div>
|
297
|
-
<div class="col-3">Source</div>
|
298
|
-
<div class="col-1">Severity</div>
|
299
|
-
</div>
|
300
|
-
<div class="queries">
|
301
|
-
<query v-for="query in queries" v-bind:query="query" v-bind:key="query.sql" v-bind:tags="tags"></query>
|
302
|
-
</div>
|
303
|
-
|
304
|
-
<div v-if="search == ''">
|
305
|
-
<div class="row">
|
306
|
-
<div class="col-12">We also found <a href="#" v-on:click.prevent="lowExpanded = !lowExpanded">{{ queriesLow.length }} queries</a> that look fine.</div>
|
307
|
-
</div>
|
308
|
-
<a name="lowExapnded"></a>
|
309
|
-
<div class="queries" v-if="lowExpanded">
|
310
|
-
<query v-for="query in queriesLow" v-bind:query="query" v-bind:key="query.sql" v-bind:tags="tags"></query>
|
311
|
-
</div>
|
312
|
-
</div>
|
313
|
-
<div style="height:50px"></div>
|
314
|
-
</div>
|
315
|
-
</div>
|
316
|
-
|
317
|
-
<script>
|
318
|
-
Vue.component('query', {
|
319
|
-
template: '#query-template',
|
320
|
-
props: ['query', 'tags', 'github'],
|
321
|
-
data: function () {
|
322
|
-
return {
|
323
|
-
expanded: false,
|
324
|
-
rawExpanded: false
|
325
|
-
};
|
326
|
-
},
|
327
|
-
methods: {
|
328
|
-
truncate: function (string, len) {
|
329
|
-
if ( string.length > len ) {
|
330
|
-
return string.substring(0, len - 3) + "...";
|
331
|
-
} else {
|
332
|
-
return string;
|
333
|
-
}
|
334
|
-
},
|
335
|
-
expandInfo: function(tag, event) {
|
336
|
-
this.$modal.show('dialog', {
|
337
|
-
title: this.tags[tag].title,
|
338
|
-
text: this.tags[tag].description,
|
339
|
-
buttons: [
|
340
|
-
{
|
341
|
-
title: 'Close'
|
342
|
-
}
|
343
|
-
]
|
344
|
-
})
|
345
|
-
event.preventDefault();
|
346
|
-
},
|
347
|
-
expandToggle: function(event) {
|
348
|
-
if (event) event.preventDefault()
|
349
|
-
this.expanded = !this.expanded;
|
350
|
-
},
|
351
|
-
shortLocation: function(query) {
|
352
|
-
if ( !query.backtrace || query.backtrace.length == 0 )
|
353
|
-
return null;
|
354
|
-
var location = query.backtrace[0];
|
355
|
-
return location.match(/([^\/]+:\d+):/)[1];
|
356
|
-
},
|
357
|
-
makeURL: function(line, content) {
|
358
|
-
if ( !data.url || !line )
|
359
|
-
return content;
|
360
|
-
|
361
|
-
var matches = line.match(/(.+):(\d+):/);
|
362
|
-
var file = matches[1].replace(/^\/+/, '');
|
363
|
-
var line = matches[2];
|
364
|
-
|
365
|
-
return `<a href='${data.url}/${file}#L${line}' target='_new'>${content}</a>`;
|
366
|
-
}
|
367
|
-
},
|
368
|
-
computed: {
|
369
|
-
expandText: function() {
|
370
|
-
return this.expanded ? "-" : "+";
|
371
|
-
}
|
372
|
-
}
|
373
|
-
});
|
374
|
-
|
375
|
-
var app = new Vue({
|
376
|
-
el: '#app',
|
377
|
-
data: {
|
378
|
-
highQ: queriesByTable,
|
379
|
-
lowQ: queriesByTableLow,
|
380
|
-
tags: data.tags,
|
381
|
-
lowExpanded: false,
|
382
|
-
hasFuzzed: queriesHaveFuzzed,
|
383
|
-
search: ''
|
384
|
-
},
|
385
|
-
methods: {
|
386
|
-
updateSearch: _.debounce(function (e) {
|
387
|
-
this.search = e.target.value;
|
388
|
-
}, 500)
|
389
|
-
},
|
390
|
-
computed: {
|
391
|
-
queries: function() {
|
392
|
-
if ( this.search != '' ) {
|
393
|
-
var filtered = [];
|
394
|
-
var lcSearch = this.search.toLowerCase();
|
395
|
-
this.highQ.concat(this.lowQ).forEach(function(q) {
|
396
|
-
if ( q.searchString.includes(lcSearch) )
|
397
|
-
filtered.push(q);
|
398
|
-
});
|
399
|
-
return filtered;
|
400
|
-
} else
|
401
|
-
return this.highQ;
|
402
|
-
},
|
403
|
-
queriesLow: function() {
|
404
|
-
return this.lowQ;
|
405
|
-
}
|
406
|
-
}
|
407
|
-
});
|
408
|
-
|
409
|
-
|
410
|
-
</script>
|
411
|
-
</body>
|
412
|
-
</html>
|