shiba 0.6.0 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- 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/package.json
CHANGED
@@ -1,20 +1,50 @@
|
|
1
1
|
{
|
2
|
-
"name": "
|
3
|
-
"version": "1.0
|
4
|
-
"
|
2
|
+
"name": "hello-world",
|
3
|
+
"version": "0.1.0",
|
4
|
+
"private": true,
|
5
5
|
"scripts": {
|
6
|
-
"
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
"license": "ISC",
|
11
|
-
"devDependencies": {
|
12
|
-
"webpack": "^4.29.0",
|
13
|
-
"webpack-cli": "^3.2.1"
|
6
|
+
"serve": "vue-cli-service serve",
|
7
|
+
"build": "vue-cli-service build",
|
8
|
+
"lint": "vue-cli-service lint",
|
9
|
+
"autobuild": "vue-cli-service build --watch"
|
14
10
|
},
|
15
11
|
"dependencies": {
|
16
|
-
"
|
17
|
-
"
|
12
|
+
"bootstrap": "^4.3.1",
|
13
|
+
"nanoajax": "^0.4.3",
|
14
|
+
"vue": "^2.6.6",
|
18
15
|
"vue-js-modal": "^1.3.28"
|
19
|
-
}
|
16
|
+
},
|
17
|
+
"devDependencies": {
|
18
|
+
"@vue/cli-plugin-babel": "^3.5.0",
|
19
|
+
"@vue/cli-plugin-eslint": "^3.5.0",
|
20
|
+
"@vue/cli-service": "^3.5.0",
|
21
|
+
"babel-eslint": "^10.0.1",
|
22
|
+
"eslint": "^5.8.0",
|
23
|
+
"eslint-plugin-vue": "^5.0.0",
|
24
|
+
"vue-template-compiler": "^2.5.21"
|
25
|
+
},
|
26
|
+
"eslintConfig": {
|
27
|
+
"root": true,
|
28
|
+
"env": {
|
29
|
+
"node": true
|
30
|
+
},
|
31
|
+
"extends": [
|
32
|
+
"plugin:vue/essential",
|
33
|
+
"eslint:recommended"
|
34
|
+
],
|
35
|
+
"rules": {},
|
36
|
+
"parserOptions": {
|
37
|
+
"parser": "babel-eslint"
|
38
|
+
}
|
39
|
+
},
|
40
|
+
"postcss": {
|
41
|
+
"plugins": {
|
42
|
+
"autoprefixer": {}
|
43
|
+
}
|
44
|
+
},
|
45
|
+
"browserslist": [
|
46
|
+
"> 1%",
|
47
|
+
"last 2 versions",
|
48
|
+
"not ie <= 8"
|
49
|
+
]
|
20
50
|
}
|
data/web/src/App.vue
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
<template>
|
2
|
+
<div id="app" v-cloak>
|
3
|
+
<v-dialog :width="600"></v-dialog>
|
4
|
+
<div class="container" style="">
|
5
|
+
<div class="row" v-if="hasFuzzed">
|
6
|
+
<div class="alert alert-warning" role="alert">
|
7
|
+
This query analysis was generated using estimated table sizes.
|
8
|
+
To improve these results and find other problem queries beyond missing indexes, we'll need more stats.<br/>
|
9
|
+
<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>
|
10
|
+
</div>
|
11
|
+
</div>
|
12
|
+
<div class="row">
|
13
|
+
<div class="col-10"></div>
|
14
|
+
<div class="col-2"><input :value="search" @input="updateSearch" placeholder="search..."></div>
|
15
|
+
</div>
|
16
|
+
<div class="row">
|
17
|
+
<div class="col-12">We found {{ queries.length }} queries that
|
18
|
+
<span v-if="search == ''">deserve your attention:</span>
|
19
|
+
<span v-else>match your search term</span>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
<div class="row">
|
23
|
+
<div class="col-3">Table</div>
|
24
|
+
<div class="col-5">Query</div>
|
25
|
+
<div class="col-3">Source</div>
|
26
|
+
<div class="col-1">Severity</div>
|
27
|
+
</div>
|
28
|
+
<div class="queries">
|
29
|
+
<Query v-for="query in queries" v-bind:query="query" v-bind:key="query.sql" v-bind:tags="tags" v-bind:url="url"></query>
|
30
|
+
</div>
|
31
|
+
<div v-if="search == ''">
|
32
|
+
<div class="row">
|
33
|
+
<div class="col-12">We also found <a href="#" v-on:click.prevent="lowExpanded = !lowExpanded">{{ queriesLow.length }} queries</a> that look fine.</div>
|
34
|
+
</div>
|
35
|
+
<a name="lowExapnded"></a>
|
36
|
+
<div class="queries" v-if="lowExpanded">
|
37
|
+
<query v-for="query in queriesLow" v-bind:query="query" v-bind:key="query.sql" v-bind:tags="tags" v-bind:url="url"></query>
|
38
|
+
</div>
|
39
|
+
</div>
|
40
|
+
<div style="height:50px"></div>
|
41
|
+
</div>
|
42
|
+
</div>
|
43
|
+
</template>
|
44
|
+
|
45
|
+
<script>
|
46
|
+
import Query from './components/Query.vue'
|
47
|
+
import registerMessage from './components/Message.js'
|
48
|
+
import QueryData from './query_data.js'
|
49
|
+
import VModal from 'vue-js-modal';
|
50
|
+
import _ from 'lodash'
|
51
|
+
import Vue from 'vue';
|
52
|
+
import nanoajax from 'nanoajax';
|
53
|
+
import 'bootstrap/dist/css/bootstrap.min.css'
|
54
|
+
|
55
|
+
Vue.use(VModal, { dialog: true });
|
56
|
+
|
57
|
+
function categorizeQueries(v, queries) {
|
58
|
+
queries.forEach(function(query) {
|
59
|
+
var q = new QueryData(query);
|
60
|
+
|
61
|
+
if ( q.severity == "none" ) {
|
62
|
+
v.lowQ.push(q);
|
63
|
+
} else {
|
64
|
+
v.highQ.push(q);
|
65
|
+
}
|
66
|
+
|
67
|
+
if ( q.hasTag("fuzzed_data") )
|
68
|
+
this.hasFuzzed = true;
|
69
|
+
|
70
|
+
var rCost = 0;
|
71
|
+
q.messages.forEach(function(m) {
|
72
|
+
if ( m.cost && m.cost != 0) {
|
73
|
+
rCost += m.cost;
|
74
|
+
m.running_cost = rCost;
|
75
|
+
} else {
|
76
|
+
m.running_cost = undefined;
|
77
|
+
}
|
78
|
+
});
|
79
|
+
});
|
80
|
+
|
81
|
+
var f = QueryData.sortByFunc(['severityIndex', 'table']);
|
82
|
+
v.highQ = v.highQ.sort(f);
|
83
|
+
v.lowQ = v.lowQ.sort(f);
|
84
|
+
}
|
85
|
+
|
86
|
+
|
87
|
+
export default {
|
88
|
+
name: 'app',
|
89
|
+
data: () => ({
|
90
|
+
highQ: [],
|
91
|
+
lowQ: [],
|
92
|
+
tags: {},
|
93
|
+
lowExpanded: false,
|
94
|
+
hasFuzzed: false,
|
95
|
+
search: '',
|
96
|
+
url: null
|
97
|
+
}),
|
98
|
+
mounted () {
|
99
|
+
if ( typeof(shibaData) === "undefined" ) {
|
100
|
+
nanoajax.ajax({url:'/example_data.json'}, function (code, responseText) {
|
101
|
+
if ( code == 200 ) {
|
102
|
+
var data = JSON.parse(responseText);
|
103
|
+
this.setupData(data);
|
104
|
+
}
|
105
|
+
}.bind(this));
|
106
|
+
} else {
|
107
|
+
// eslint-disable-next-line
|
108
|
+
this.setupData(shibaData);
|
109
|
+
}
|
110
|
+
},
|
111
|
+
methods: {
|
112
|
+
setupData: function(data) {
|
113
|
+
this.url = data.url;
|
114
|
+
this.tags = data.tags;
|
115
|
+
|
116
|
+
Object.keys(this.tags).forEach((k) => {
|
117
|
+
registerMessage(k, this.tags[k].title, this.tags[k].summary);
|
118
|
+
})
|
119
|
+
categorizeQueries(this, data.queries);
|
120
|
+
},
|
121
|
+
updateSearch: _.debounce(function (e) {
|
122
|
+
this.search = e.target.value;
|
123
|
+
}, 500)
|
124
|
+
},
|
125
|
+
computed: {
|
126
|
+
queries: function() {
|
127
|
+
if ( this.search != '' ) {
|
128
|
+
var filtered = [];
|
129
|
+
var lcSearch = this.search.toLowerCase();
|
130
|
+
this.highQ.concat(this.lowQ).forEach(function(q) {
|
131
|
+
if ( q.searchString.includes(lcSearch) )
|
132
|
+
filtered.push(q);
|
133
|
+
});
|
134
|
+
return filtered;
|
135
|
+
} else
|
136
|
+
return this.highQ;
|
137
|
+
},
|
138
|
+
queriesLow: function() {
|
139
|
+
return this.lowQ;
|
140
|
+
}
|
141
|
+
},
|
142
|
+
components: {
|
143
|
+
Query
|
144
|
+
}
|
145
|
+
}
|
146
|
+
</script>
|
147
|
+
|
148
|
+
<style>
|
149
|
+
.sql {
|
150
|
+
font-family: monospace;
|
151
|
+
}
|
152
|
+
|
153
|
+
.badge {
|
154
|
+
color: black;
|
155
|
+
background-color: white;
|
156
|
+
border-style: solid;
|
157
|
+
border-width: 1.5px;
|
158
|
+
margin-right: 5px;
|
159
|
+
width: 100px;
|
160
|
+
}
|
161
|
+
|
162
|
+
.shiba-badge-td {
|
163
|
+
width: 100px;
|
164
|
+
}
|
165
|
+
|
166
|
+
.shiba-messages {
|
167
|
+
margin: 0px;
|
168
|
+
margin-top: 10px;
|
169
|
+
width: 100%;
|
170
|
+
}
|
171
|
+
|
172
|
+
.shiba-messages td {
|
173
|
+
}
|
174
|
+
|
175
|
+
.shiba-message {
|
176
|
+
padding-right: 10px;
|
177
|
+
width: 90%;
|
178
|
+
}
|
179
|
+
|
180
|
+
.running-totals {
|
181
|
+
align: right;
|
182
|
+
font-family: monospace;
|
183
|
+
}
|
184
|
+
[v-cloak] { display: none }
|
185
|
+
</style>
|
Binary file
|
@@ -0,0 +1,36 @@
|
|
1
|
+
<template>
|
2
|
+
<div>
|
3
|
+
Stack Trace:<br>
|
4
|
+
<div class="backtrace">
|
5
|
+
<div v-for="bt in backtrace" v-bind:key="bt" v-html="makeURL(bt, bt)"></div>
|
6
|
+
</div>
|
7
|
+
</div>
|
8
|
+
</template>
|
9
|
+
|
10
|
+
<script>
|
11
|
+
export default {
|
12
|
+
name: 'Backtrace',
|
13
|
+
props: ['url', 'backtrace'],
|
14
|
+
methods: {
|
15
|
+
makeURL: function(line, content) {
|
16
|
+
if ( !this.url || !line )
|
17
|
+
return content;
|
18
|
+
|
19
|
+
var matches = line.match(/(.+):(\d+):/);
|
20
|
+
var f = matches[1].replace(/^\/+/, '');
|
21
|
+
var l = matches[2];
|
22
|
+
|
23
|
+
return `<a href='${this.url}/${f}#L${l}' target='_new'>${content}</a>`;
|
24
|
+
}
|
25
|
+
}
|
26
|
+
}
|
27
|
+
</script>
|
28
|
+
<style>
|
29
|
+
.backtrace {
|
30
|
+
font-family: monospace;
|
31
|
+
background-color: #EEEEEE;
|
32
|
+
padding: 5px;
|
33
|
+
margin: 10px
|
34
|
+
}
|
35
|
+
</style>
|
36
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="hello">
|
3
|
+
<h1>{{ msg }}</h1>
|
4
|
+
<p>
|
5
|
+
For a guide and recipes on how to configure / customize this project,<br>
|
6
|
+
check out the
|
7
|
+
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
8
|
+
</p>
|
9
|
+
<h3>Installed CLI Plugins</h3>
|
10
|
+
<ul>
|
11
|
+
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
12
|
+
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
|
13
|
+
</ul>
|
14
|
+
<h3>Essential Links</h3>
|
15
|
+
<ul>
|
16
|
+
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
17
|
+
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
18
|
+
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
19
|
+
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
20
|
+
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
21
|
+
</ul>
|
22
|
+
<h3>Ecosystem</h3>
|
23
|
+
<ul>
|
24
|
+
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
25
|
+
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
26
|
+
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
27
|
+
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
28
|
+
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
29
|
+
</ul>
|
30
|
+
</div>
|
31
|
+
</template>
|
32
|
+
|
33
|
+
<script>
|
34
|
+
export default {
|
35
|
+
name: 'HelloWorld',
|
36
|
+
props: {
|
37
|
+
msg: String
|
38
|
+
}
|
39
|
+
}
|
40
|
+
</script>
|
41
|
+
|
42
|
+
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
43
|
+
<style scoped>
|
44
|
+
h3 {
|
45
|
+
margin: 40px 0 0;
|
46
|
+
}
|
47
|
+
ul {
|
48
|
+
list-style-type: none;
|
49
|
+
padding: 0;
|
50
|
+
}
|
51
|
+
li {
|
52
|
+
display: inline-block;
|
53
|
+
margin: 0 10px;
|
54
|
+
}
|
55
|
+
a {
|
56
|
+
color: #42b983;
|
57
|
+
}
|
58
|
+
</style>
|
@@ -0,0 +1,104 @@
|
|
1
|
+
var template = `
|
2
|
+
<tr>
|
3
|
+
<td class="shiba-badge-td">
|
4
|
+
<a class="badge" v-bind:style="costToColor">::TITLE::</a>
|
5
|
+
</td>
|
6
|
+
<td class="shiba-message">
|
7
|
+
::SUMMARY::
|
8
|
+
</td>
|
9
|
+
<td class="running-totals">
|
10
|
+
{{ formattedRunningCost }}
|
11
|
+
</td>
|
12
|
+
</tr>
|
13
|
+
`
|
14
|
+
|
15
|
+
var greenToRedGradient = [
|
16
|
+
'#57bb8a','#63b682', '#73b87e', '#84bb7b', '#94bd77', '#a4c073', '#b0be6e',
|
17
|
+
'#c4c56d', '#d4c86a', '#e2c965', '#f5ce62', '#f3c563', '#e9b861', '#e6ad61',
|
18
|
+
'#ecac67', '#e9a268', '#e79a69', '#e5926b', '#e2886c', '#e0816d', '#dd776e'
|
19
|
+
];
|
20
|
+
|
21
|
+
var templateComputedFunctions = {
|
22
|
+
key_parts: function() {
|
23
|
+
if ( this.index_used && this.index_used.length > 0 )
|
24
|
+
return this.index_used.join(',');
|
25
|
+
else
|
26
|
+
return "";
|
27
|
+
},
|
28
|
+
fuzz_table_sizes: function() {
|
29
|
+
var h = {};
|
30
|
+
var tables = this.tables;
|
31
|
+
|
32
|
+
Object.keys(tables).forEach(function(k) {
|
33
|
+
var size = tables[k];
|
34
|
+
if ( !h[size] )
|
35
|
+
h[size] = [];
|
36
|
+
|
37
|
+
h[size].push(k);
|
38
|
+
});
|
39
|
+
|
40
|
+
var sizesDesc = Object.keys(h).sort(function(a, b) { return b - a });
|
41
|
+
var str = "";
|
42
|
+
|
43
|
+
sizesDesc.forEach(function(size) {
|
44
|
+
str = str + h[size].join(", ") + ": " + size.toLocaleString() + " rows. ";
|
45
|
+
});
|
46
|
+
|
47
|
+
return str;
|
48
|
+
},
|
49
|
+
formatted_cost: function() {
|
50
|
+
var readPercentage = (this.rows_read / this.table_size) * 100.0;
|
51
|
+
if ( this.rows_read > 100 && readPercentage > 1 ) // todo: make better
|
52
|
+
return `${readPercentage.toFixed()}% (${this.rows_read.toLocaleString()}) of the`;
|
53
|
+
else
|
54
|
+
return this.rows_read.toLocaleString();
|
55
|
+
},
|
56
|
+
costToColor: function() {
|
57
|
+
var costScale = this.cost ? this.cost / 0.5 : 0;
|
58
|
+
|
59
|
+
if ( costScale > 1 )
|
60
|
+
costScale = 1;
|
61
|
+
|
62
|
+
var pos = (costScale * (greenToRedGradient.length - 1)).toFixed();
|
63
|
+
|
64
|
+
return "border-color: " + greenToRedGradient[pos];
|
65
|
+
},
|
66
|
+
formattedRunningCost: function() {
|
67
|
+
if ( this.running_cost === undefined )
|
68
|
+
return "-";
|
69
|
+
else if ( this.running_cost < 1.0 )
|
70
|
+
return (this.running_cost * 100).toFixed() + "ms";
|
71
|
+
else
|
72
|
+
return this.running_cost.toFixed(1) + "s";
|
73
|
+
},
|
74
|
+
formatted_result: function() {
|
75
|
+
var rb = this.result_bytes;
|
76
|
+
var result;
|
77
|
+
if ( rb == 0 )
|
78
|
+
return "" + this.result_size + " rows";
|
79
|
+
else if ( rb < 1000 )
|
80
|
+
result = rb + " bytes ";
|
81
|
+
else if ( rb < 1000000 )
|
82
|
+
result = (rb / 1000).toFixed() + "kb ";
|
83
|
+
else
|
84
|
+
result = (rb / 1000000 ).toFixed(1) + "mb ";
|
85
|
+
|
86
|
+
return result + " (" + this.result_size.toLocaleString() + " rows)";
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
import Vue from 'vue'
|
91
|
+
|
92
|
+
export default function (tag, title, summary) {
|
93
|
+
var tmpl = template.replace("::TITLE::", title).replace("::SUMMARY::", summary);
|
94
|
+
|
95
|
+
Vue.component(`tag-${tag}`, {
|
96
|
+
template: tmpl,
|
97
|
+
props: [ 'table_size', 'result_size', 'table', 'cost', 'index', 'join_to', 'index_used', 'running_cost', 'tables', 'rows_read', 'result_bytes' ],
|
98
|
+
computed: templateComputedFunctions,
|
99
|
+
data: function () {
|
100
|
+
return { lastRunnningCost: undefined };
|
101
|
+
}
|
102
|
+
});
|
103
|
+
}
|
104
|
+
|