runtime_profiler 0.1.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e33ac72fec2841f6fb3c5b50ae46aece8bd6902ff177392dfc056e7f6ae336d6
4
- data.tar.gz: 1053411fdb7d59d1c54f9c89cd1f465dbf8cc1b280bf9d42a93ee5055fc223bf
3
+ metadata.gz: 7839cd9d608c73c35a269e4bf7f2f52ae482e714dd99a108e9078e6628f9a86b
4
+ data.tar.gz: 9eecf865436dcd07b575d96c1bfb21d1ba1c9344c9100aa955ed00a84d2e8011
5
5
  SHA512:
6
- metadata.gz: 430be7e054bd7748c36bf468bf28d134d35a43a441d5d3fff6730e29d15d173a74ce5e64b8e3711ff63291b10dc8c3977f58a3caecff6c649936f632d7b15be7
7
- data.tar.gz: b2d7f4f5019f489def998d8858bbec09c99be054289429482858836a0e88a2e49e22626e2f31461cedf4e9f5473eafb8baac4e0818528c2aec04a824d68683d7
6
+ metadata.gz: eaeedf57a5c02b27e1f75778290f821a4b0ed3901220c7dce8529df44d4bf49fe8d0846a4bb3e7e73b485c96196f85d7d57a6cc3e8df93bfda8b05676526ddc1
7
+ data.tar.gz: 196e71a0244d7d47c8af6585213c45373dcc8d694bac70daa86e9688877d4b02fc029bc50df87baf3ce288e64c8c7850ea717603975eb38a9da367e34998d9a5
@@ -0,0 +1,18 @@
1
+ version: v1.0
2
+ name: Ruby
3
+ agent:
4
+ machine:
5
+ type: e1-standard-2
6
+ os_image: ubuntu1804
7
+ blocks:
8
+ - name: test
9
+ task:
10
+ jobs:
11
+ - name: test
12
+ commands:
13
+ - checkout
14
+ - sem-version ruby 2.6.5
15
+ - cache restore
16
+ - bundle install --path vendor/bundle
17
+ - cache store
18
+ - bundle exec rake
data/README.md CHANGED
@@ -1,13 +1,9 @@
1
- # runtime_profiler - Runtime Profiler for Rails Applications
1
+ # runtime_profiler - Runtime Profiler for Rails Applications [![Build Status](https://wnuqui.semaphoreci.com/badges/runtime_profiler/branches/master.svg?style=shields)](https://wnuqui.semaphoreci.com/projects/runtime_profiler)
2
2
 
3
- `runtime_profiler` instruments api or a method of your Rails application using Rails' `ActiveSupport::Notifications`
3
+ `runtime_profiler` instruments API endpoints or methods in your Rails application using Rails' `ActiveSupport::Notifications`. Currently, it is intended to be used during development or test.
4
4
 
5
5
  It then aggregates and generates report to give you insights about specific calls in your Rails application.
6
6
 
7
- ## Note
8
-
9
- This is still a **work in progress**. However, this is a tool meant to be used in development so it is safe to use.
10
-
11
7
  ## Installation
12
8
 
13
9
  Add this line to your application's Gemfile:
@@ -25,9 +21,9 @@ And then execute:
25
21
 
26
22
  ## Profiling/Instrumenting
27
23
 
28
- To start profiling, make a test and use `RuntimeProfiler.profile!` method in the tests. The output of instrumentation will be generated under the `tmp` folder of your application.
24
+ To start profiling, you can make a test that targets a particular endpoint and use `RuntimeProfiler.profile!` method in the test. The output of instrumentation will be generated under the `tmp` folder of your application.
29
25
 
30
- Example:
26
+ Example of a test code wrap by `RuntimeProfiler.profile!` method:
31
27
  ```ruby
32
28
  it 'updates user' do
33
29
  RuntimeProfiler.profile!('updates user', [User]) {
@@ -38,19 +34,72 @@ it 'updates user' do
38
34
  end
39
35
  ```
40
36
 
41
- Run tests as usual and follow printed instructions after running tests.
37
+ Run the test as usual and follow printed instructions after running.
42
38
 
43
- ## Reporting
39
+ If you prefer writing just code snippet, then just wrap the snippet with `RuntimeProfiler.profile!` method:
40
+ ```ruby
41
+ RuntimeProfiler.profile!('UserMailer', [UserMailer]) {
42
+ user = User.last
43
+ UserMailer.with(user: user).weekly_summary.deliver_now
44
+ }
45
+ ```
44
46
 
45
- To see profiling/instrumenting report, please open the report in browser with JSON viewer report. Or you can run the following command:
47
+ **Note:** The code (tests or not) where `RuntimeProfiler.profile!` is used must be **free from any mocking** since your goal is to check bottlenecks.
48
+
49
+ ## Viewing Profiling Result
50
+
51
+ To see profiling/instrumenting report, you can open the report in browser with JSON viewer report. Or you can run the following command:
46
52
 
47
53
  ```bash
48
54
  bundle exec runtime_profiler view ~/the-rails-app/tmp/runtime-profiling-51079-1521371428.json
49
55
  ```
50
56
 
57
+ ### view options
58
+
59
+ Here are the command line options for `runtime_profiler view` command.
60
+
61
+ ```bash
62
+ $ bundle exec runtime_profiler view --help
63
+
64
+ NAME:
65
+
66
+ view
67
+
68
+ SYNOPSIS:
69
+
70
+ runtime_profiler view <profile.report.json> [options]
71
+
72
+ DESCRIPTION:
73
+
74
+ Display report in console given the JSON report file
75
+
76
+ OPTIONS:
77
+
78
+ --sort-by COLUMN
79
+ Sort by COLUMN. COLUMN can be "max_runtime", total_calls" or "total_runtime". Default is "max_runtime".
80
+
81
+ --details TYPE
82
+ TYPE can be "full" or "summary". Default is "summary"
83
+
84
+ --only-sqls
85
+ Show only SQL queries. Default is false.
86
+
87
+ --only-methods
88
+ Show only methods. Default is false.
89
+
90
+ --runtime-above RUNTIME
91
+ RUNTIME is integer or float value in ms.
92
+
93
+ --calls-above CALLS
94
+ CALLS is integer value.
95
+
96
+ --rounding ROUNDING
97
+ ROUNDING is integer value. Used in rounding runtimes. Default is 4.
98
+ ```
99
+
51
100
  ## Configurations
52
101
 
53
- All the configurable variables and their defaults are listed below:
102
+ All the configurable variables and their defaults are listed below. These configurations can be put in the `config/initializers` folder of your Rails application.
54
103
  ```ruby
55
104
  RuntimeProfiler.output_path = File.join(Rails.root.to_s, 'tmp')
56
105
  RuntimeProfiler.instrumented_constants = [User]
@@ -41,17 +41,5 @@ module RuntimeProfiler
41
41
 
42
42
  profiler.save_instrumentation_data
43
43
  end
44
-
45
- def runtime(label='for the block')
46
- result = nil
47
-
48
- elapsed_time = Benchmark.realtime { result = yield }
49
-
50
- puts
51
- puts '~~~~> ELAPSED TIME (%s): %s' % [label, elapsed_time * 1000]
52
- puts
53
-
54
- result
55
- end
56
44
  end
57
45
  end
@@ -16,28 +16,33 @@ module RuntimeProfiler
16
16
  c.syntax = 'runtime_profiler view <profile.report.json> [options]'
17
17
  c.description = 'Display report in console given the JSON report file'
18
18
 
19
- c.option '--sort-by COLUMN', String, 'Sort by COLUMN. COLUMN can either be "total_calls" or "total_runtime". Default is "total_calls".'
20
- c.option '--details TYPE', String, 'TYPE can be "full" or "summary".'
21
- c.option '--runtime-above RUNTIME', String, 'RUNTIME is numeric value in ms.'
22
- c.option '--only-sqls', String, 'Show only SQL(s).'
23
- c.option '--only-methods', String, 'Show only method(s).'
24
- c.option '--calls-above CALLS', String, 'CALLS is numeric value.'
19
+ c.option '--sort-by COLUMN', String, 'Sort by COLUMN. COLUMN can be "max_runtime", total_calls" or "total_runtime". Default is "max_runtime".'
20
+ c.option '--details TYPE', String, 'TYPE can be "full" or "summary". Default is "summary"'
21
+ c.option '--only-sqls', String, 'Show only SQL queries. Default is false.'
22
+ c.option '--only-methods', String, 'Show only methods. Default is false.'
23
+ c.option '--runtime-above RUNTIME', Float, 'RUNTIME is integer or float value in ms.'
24
+ c.option '--calls-above CALLS', Integer, 'CALLS is integer value.'
25
+ c.option '--rounding ROUNDING', Integer, 'ROUNDING is integer value. Used in rounding runtimes. Default is 4.'
25
26
 
26
27
  c.action do |args, options|
27
28
  default_options = {
28
- sort_by: 'total_calls',
29
+ sort_by: 'max_runtime',
29
30
  details: 'summary',
30
31
  runtime_above: 0,
31
32
  only_sqls: false,
32
33
  only_methods: false,
33
- calls_above: 1
34
+ calls_above: 0,
35
+ rounding: 4
34
36
  }
35
37
 
36
38
  options.default default_options
37
39
 
38
- report = RuntimeProfiler::TextReport.new(args.first, options)
39
-
40
- report.print
40
+ if args.first.nil?
41
+ say 'You need to supply <profile.report.json> as first argument of view.'
42
+ else
43
+ report = RuntimeProfiler::TextReport.new(args.first, options)
44
+ report.print
45
+ end
41
46
  end
42
47
  end
43
48
 
@@ -3,12 +3,15 @@ module RuntimeProfiler
3
3
  attr_reader :controller_data, :sql_data
4
4
 
5
5
  def initialize(controller_data: nil, sql_data: nil)
6
- @controller_data = controller_data
6
+ @controller_data = controller_data || {}
7
7
  @sql_data = sql_data
8
8
  end
9
9
 
10
10
  def persist!
11
- instrumented_api = [controller_data[:payload][:controller], controller_data[:payload][:action]].join('#')
11
+ instrumented_api = [
12
+ controller_data[:payload][:controller],
13
+ controller_data[:payload][:action]
14
+ ].join('#') if controller_data[:payload]
12
15
 
13
16
  instrumentation_data = {
14
17
  instrumentation: {
@@ -38,7 +41,8 @@ module RuntimeProfiler
38
41
  f.write JSON.dump(instrumentation_data)
39
42
  end
40
43
 
41
- puts '~~~~> [ Profiling RUNTIME ] Profiling now COMPLETE and JSON report written at ' + output_file.to_s
44
+ puts "\n"
45
+ puts '~~~~> [ Profiling RUNTIME ] Profiling now COMPLETE and JSON report is written at ' + output_file.to_s
42
46
  puts '~~~~> [ Profiling RUNTIME ]'
43
47
  puts '~~~~> [ Profiling RUNTIME ] You can do the following to view the JSON report in console:'
44
48
  puts '~~~~> [ Profiling RUNTIME ]'
@@ -1,6 +1,7 @@
1
1
  require 'hirb'
2
2
  require 'terminal-table'
3
3
  require 'active_support/core_ext/string'
4
+ require 'pry'
4
5
 
5
6
  module RuntimeProfiler
6
7
  class TextReport
@@ -8,17 +9,17 @@ module RuntimeProfiler
8
9
  DURATION_WIDTH = 22
9
10
  TOTAL_RUNTIME_WIDTH = 20
10
11
 
11
- SUMMARY_TEMPLATE = <<-EOT.strip_heredoc
12
+ FULL_DETAILS_TEMPLATE = <<-EOT.strip_heredoc
12
13
 
13
14
  \e[1mPROFILING REPORT\e[22m
14
15
  ----------------
15
16
 
16
- \e[1mRUNTIME\e[22m
17
+ \e[1mAPI RUNTIME\e[22m
17
18
  Total Runtime : %s ms
18
19
  Database Runtime : %s ms
19
20
  View Runtime : %s ms
20
21
 
21
- \e[1mMETHODS\e[22m
22
+ \e[1mMETHOD CALLS\e[22m
22
23
  SLOWEST : %s (%s ms)
23
24
  MOSTLY CALLED : %s (%s number of calls in %s ms)
24
25
 
@@ -39,48 +40,121 @@ module RuntimeProfiler
39
40
 
40
41
  EOT
41
42
 
43
+ METHODS_DETAILS_TEMPLATE = <<-EOT.strip_heredoc
44
+
45
+ \e[1mPROFILING REPORT\e[22m
46
+ ----------------
47
+
48
+ \e[1mAPI RUNTIME\e[22m
49
+ Total Runtime : %s ms
50
+ Database Runtime : %s ms
51
+ View Runtime : %s ms
52
+
53
+ \e[1mMETHOD CALLS\e[22m
54
+ SLOWEST : %s (%s ms)
55
+ MOSTLY CALLED : %s (%s number of calls in %s ms)
56
+
57
+ EOT
58
+
59
+ SQLS_DETAILS_TEMPLATE = <<-EOT.strip_heredoc
60
+
61
+ \e[1mPROFILING REPORT\e[22m
62
+ ----------------
63
+
64
+ \e[1mAPI RUNTIME\e[22m
65
+ Total Runtime : %s ms
66
+ Database Runtime : %s ms
67
+ View Runtime : %s ms
68
+
69
+ \e[1mSQL CALLS\e[22m
70
+ Total : %s
71
+ Total Unique : %s
72
+
73
+ \e[1mSLOWEST\e[22m
74
+ Total Runtime : %s ms
75
+ SQL : %s
76
+ Source : %s
77
+
78
+ \e[1mMOSTLY CALLED\e[22m
79
+ Total Calls : %s
80
+ Total Runtime : %s ms
81
+ SQL : %s
82
+ Sources : %s
83
+
84
+ EOT
85
+
42
86
  attr_accessor :data, :options
43
87
 
44
88
  def initialize(json_file, options)
45
89
  self.data = JSON.parse( File.read(json_file) )
46
- self.options = options
90
+ self.options = options
47
91
  end
48
92
 
49
93
  def print
50
94
  print_summary
51
95
 
52
- if self.options.details == 'full'
53
- print_instrumented_methods
54
- print_instrumented_sql_calls
96
+ if options.details == 'full'
97
+ if only_methods?
98
+ print_instrumented_methods
99
+ elsif only_sqls?
100
+ print_instrumented_sql_calls
101
+ else
102
+ print_instrumented_methods
103
+ print_instrumented_sql_calls
104
+ end
55
105
  end
56
106
  end
57
107
 
58
108
  private
59
109
 
110
+ def only_methods?
111
+ options.only_methods.present? && options.only_sqls.blank?
112
+ end
113
+
114
+ def only_sqls?
115
+ options.only_sqls.present? && options.only_methods.blank?
116
+ end
117
+
118
+ def rounding
119
+ options.rounding
120
+ end
121
+
60
122
  def print_summary
61
- summary = SUMMARY_TEMPLATE % summary_template_data
123
+ summary = if only_methods?
124
+ METHODS_DETAILS_TEMPLATE % details_template_data
125
+ elsif only_sqls?
126
+ SQLS_DETAILS_TEMPLATE % details_template_data
127
+ else
128
+ FULL_DETAILS_TEMPLATE % details_template_data
129
+ end
62
130
  puts summary
63
131
  end
64
132
 
65
133
  def print_instrumented_methods
66
134
  instrumented_methods = []
67
135
 
68
- self.data['instrumentation']['instrumented_methods'].each do |profiled_object_name, methods|
69
- instrumented_methods.concat methods.map { |method| method['method'] = [profiled_object_name, method['method']].join; method}
136
+ data['instrumentation']['instrumented_methods'].each do |profiled_object_name, methods|
137
+ _instrumented_methods = methods.map do |method|
138
+ method['method'] = [profiled_object_name, method['method']].join
139
+ method
140
+ end
141
+ instrumented_methods.concat(_instrumented_methods)
70
142
  end
71
143
 
144
+ instrumented_methods = runtime_above(instrumented_methods) if options.runtime_above.presence > 0
145
+ instrumented_methods = calls_above(instrumented_methods) if options.calls_above.presence > 0
72
146
  instrumented_methods = sort(instrumented_methods)
73
147
 
74
148
  table = Terminal::Table.new do |t|
75
- t.headings = ['Method', 'Total Runtime (ms)', 'Total Calls', 'Min', 'Max']
149
+ t.headings = ['Method', 'Total Runtime (ms)', 'Total Calls', 'Min (ms)', 'Max (ms)']
76
150
 
77
151
  instrumented_methods.each_with_index do |row, index|
78
152
  t.add_row [
79
153
  row['method'],
80
- row['total_runtime'],
154
+ row['total_runtime'].round(rounding),
81
155
  row['total_calls'],
82
- row['min'],
83
- row['max']
156
+ row['min'].round(rounding),
157
+ row['max'].round(rounding)
84
158
  ]
85
159
  t.add_separator if index < instrumented_methods.size - 1
86
160
  end
@@ -95,7 +169,11 @@ module RuntimeProfiler
95
169
  end
96
170
 
97
171
  def print_instrumented_sql_calls
98
- instrumented_sql_calls = sort(self.data['instrumentation']['instrumented_sql_calls'])
172
+ instrumented_sql_calls = data['instrumentation']['instrumented_sql_calls']
173
+
174
+ instrumented_sql_calls = runtime_above(instrumented_sql_calls) if options.runtime_above.presence > 0
175
+ instrumented_sql_calls = calls_above(instrumented_sql_calls) if options.calls_above.presence > 0
176
+ instrumented_sql_calls = sort(instrumented_sql_calls, false)
99
177
 
100
178
  table = Terminal::Table.new do |t|
101
179
  t.headings = ['Count', 'Total Runtime (ms)', 'Average Runtime (ms)', 'SQL Query', 'Source']
@@ -103,7 +181,8 @@ module RuntimeProfiler
103
181
  instrumented_sql_calls.each_with_index do |row, index|
104
182
  chopped_sql = wrap_text(row['sql'], sql_width)
105
183
  source_list = wrap_list(row['runtimes'].map { |runtime| runtime[1] }.uniq, sql_width - 15)
106
- average_runtime = row['average'].round(2)
184
+ average_runtime = row['average'].round(rounding)
185
+ total_runtime = row['total_runtime'].round(rounding)
107
186
  total_lines = if chopped_sql.length >= source_list.length
108
187
  chopped_sql.length
109
188
  else
@@ -113,14 +192,14 @@ module RuntimeProfiler
113
192
  (0...total_lines).each do |line|
114
193
  count = line == 0 ? row['total_calls'] : ''
115
194
  average = line == 0 ? average_runtime : ''
116
- total_runtime = line == 0 ? row['total_runtime'] : ''
195
+ total_runtime = line == 0 ? total_runtime : ''
117
196
  source = source_list.length > line ? source_list[line] : ''
118
197
  query = row['sql'].length > line ? chopped_sql[line] : ''
119
198
 
120
199
  t.add_row []
121
200
  t.add_row [count, total_runtime, average, query, source]
122
201
  end
123
-
202
+
124
203
  t.add_row []
125
204
  t.add_separator if index < instrumented_sql_calls.size - 1
126
205
  end
@@ -152,43 +231,72 @@ module RuntimeProfiler
152
231
  end
153
232
  end
154
233
 
155
- def sort(data)
156
- data.sort_by do |d|
157
- if self.options.sort_by == 'total_runtime'
158
- -d['total_runtime']
159
- else
160
- -d['total_calls']
234
+ def sort(data, methods=true)
235
+ if methods
236
+ data.sort_by do |d|
237
+ if options.sort_by == 'max_runtime'
238
+ -d['max']
239
+ elsif options.sort_by == 'total_runtime'
240
+ -d['total_runtime']
241
+ elsif options.sort_by == 'total_calls'
242
+ -d['total_calls']
243
+ end
161
244
  end
245
+ else
246
+ options.sort_by = 'total_runtime' if options.sort_by == 'max_runtime'
247
+ data.sort_by { |d| options.sort_by == 'total_runtime' ? -d['total_runtime'] : -d['total_calls'] }
162
248
  end
163
249
  end
164
250
 
165
- def summary_template_data
166
- summary = self.data['instrumentation']['summary']
251
+ def runtime_above(data)
252
+ data.select { |d| d['total_runtime'] > options.runtime_above }
253
+ end
167
254
 
168
- [
169
- summary['total_runtime'].round(2),
170
- summary['db_runtime'].round(2),
171
- summary['view_runtime'].round(2),
255
+ def calls_above(data)
256
+ data.select { |d| d['total_calls'] > options.calls_above }
257
+ end
258
+
259
+ def details_template_data
260
+ summary = data['instrumentation']['summary']
261
+
262
+ template_data = [
263
+ summary['total_runtime'] ? summary['total_runtime'].round(rounding) : 'n/a',
264
+ summary['db_runtime'] ? summary['db_runtime'].round(rounding) : 'n/a',
265
+ summary['view_runtime'] ? summary['view_runtime'].round(rounding) : 'n/a'
266
+ ]
172
267
 
268
+ methods_data = [
173
269
  summary['slowest_method']['method'],
174
- summary['slowest_method']['total_runtime'].round(2),
270
+ summary['slowest_method']['total_runtime'].round(rounding),
175
271
 
176
272
  summary['mostly_called_method']['method'],
177
273
  summary['mostly_called_method']['total_calls'],
178
- summary['mostly_called_method']['total_runtime'].round(2),
274
+ summary['mostly_called_method']['total_runtime'].round(rounding)
275
+ ]
179
276
 
277
+ sqls_data = [
180
278
  summary['total_sql_calls'],
181
279
  summary['total_unique_sql_calls'],
182
280
 
183
- summary['slowest_sql']['total_runtime'].round(2),
281
+ summary['slowest_sql']['total_runtime'].round(rounding),
184
282
  summary['slowest_sql']['sql'],
185
283
  summary['slowest_sql']['source'],
186
284
 
187
285
  summary['mostly_called_sql']['total_calls'],
188
- summary['mostly_called_sql']['total_runtime'].round(2),
286
+ summary['mostly_called_sql']['total_runtime'].round(rounding),
189
287
  summary['mostly_called_sql']['sql'],
190
288
  summary['mostly_called_sql']['runtimes'].map { |runtime| runtime[1] }.uniq
191
289
  ]
290
+
291
+ if only_methods?
292
+ template_data.concat(methods_data)
293
+ elsif only_sqls?
294
+ template_data.concat(sqls_data)
295
+ else
296
+ template_data
297
+ .concat(methods_data)
298
+ .concat(sqls_data)
299
+ end
192
300
  end
193
301
  end
194
302
  end
@@ -1,3 +1,3 @@
1
1
  module RuntimeProfiler
2
- VERSION = '0.1.4'.freeze
2
+ VERSION = '0.3.0'.freeze
3
3
  end
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.executables = ['runtime_profiler']
20
20
  spec.require_paths = ['lib']
21
21
 
22
- spec.add_development_dependency 'bundler', '~> 1.12'
22
+ spec.add_development_dependency 'bundler'
23
23
  spec.add_development_dependency 'rake', '>= 12.3.3'
24
24
  spec.add_development_dependency 'minitest', '~> 5.0'
25
25
  spec.add_development_dependency 'activesupport', '>= 3.0.0'
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: runtime_profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wilfrido T. Nuqui Jr.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-14 00:00:00.000000000 Z
11
+ date: 2020-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.12'
19
+ version: '0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.12'
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -173,6 +173,7 @@ extensions: []
173
173
  extra_rdoc_files: []
174
174
  files:
175
175
  - ".gitignore"
176
+ - ".semaphore/semaphore.yml"
176
177
  - Gemfile
177
178
  - LICENSE.txt
178
179
  - README.md