rack-mini-profiler 1.1.6 → 2.0.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: 343dec64a1a5e96e086a3cbceb07c418d08a891d5f89a9315472108e125a9282
4
- data.tar.gz: d242ccef7bd8c5aeb2224f7912886a0e05ce21ef571a3f6c76f384686b5f1525
3
+ metadata.gz: 93c2989b331d19e51611a97941ea8259812285211b00ad25bb56e55a7fe17b37
4
+ data.tar.gz: '07594d8609bad0a73f86cf56cfe1bf1aaeb8e279edeba50e2a5c4a3bf86be0fb'
5
5
  SHA512:
6
- metadata.gz: 394c8ce3d60dbb2638fd0ea58420e8372702cfa5043bf08bdd51e731abe3019f1299bb4e3c22b92fac70f477a2fe8783720a3f2c46d393dda7ace0509b02963b
7
- data.tar.gz: cf3ba12543795194bab109ff9fddd97ff0d0c349b8f505cebae202cf30b6e6d8e5058a1385ad4a8cbc747d27108c6351a4c7ddb53ccda14b9f68b7f4f9978b48
6
+ metadata.gz: f651aeb06ef7bab9e853aacc4ed3ab22b8470b3b8723b7d7f727092c6e7e35db75003cdb8bbeba2ef94637c9ff85ea5bf269cbefa748f46aaeb72208d900b387
7
+ data.tar.gz: ffe60bc20d0132e5e5c34e55debb739da0f3a62fce0d9a391f515fe9ba52f9c6c8f2cd3f327e433a8f2a9222bb60b1bbede8f1f28a9b9f39cb442b4097b982e1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 2.0.0 - 2020-03-11
4
+
5
+ - [FEATURE] Prepend Net::HTTP patch instead of class_eval and aliasing (#429)
6
+ - [FEATURE] Stop patching Rails and use `ActiveSupport::Notifications` by default (see README.md for details)
7
+
3
8
  ## 1.1.6 - 2020-01-30
4
9
 
5
10
  - [FIX] edge condition on page transition function could lead to exceptions
data/README.md CHANGED
@@ -51,6 +51,20 @@ gem 'stackprof'
51
51
 
52
52
  All you have to do is to include the Gem and you're good to go in development. See notes below for use in production.
53
53
 
54
+ #### Upgrading to version 2.0.0
55
+
56
+ Prior to version 2.0.0, Mini Profiler patched various Rails methods to get the information it needed such as template rendering time. Starting from version 2.0.0, Mini Profiler doesn't patch any Rails methods by default and relies on `ActiveSupport::Notifications` to get the information it needs from Rails. If you want Mini Profiler to keep using its patches in version 2.0.0 and later, change the gem line in your `Gemfile` to the following:
57
+
58
+ If you want to manually require Mini Profiler:
59
+ ```ruby
60
+ gem 'rack-mini-profiler', require: ['enable_rails_patches']
61
+ ```
62
+
63
+ If you don't want to manually require Mini Profiler:
64
+ ```ruby
65
+ gem 'rack-mini-profiler', require: ['enable_rails_patches', 'rack-mini-profiler']
66
+ ```
67
+
54
68
  #### Rails and manual initialization
55
69
 
56
70
  In case you need to make sure rack_mini_profiler is initialized after all other gems, or you want to execute some code before rack_mini_profiler required:
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class MiniProfiler
5
+ ENABLE_RAILS_PATCHES = true
6
+ end
7
+ end
@@ -5,6 +5,11 @@ module Rack
5
5
  class << self
6
6
 
7
7
  include Rack::MiniProfiler::ProfilingMethods
8
+ attr_accessor :subscribe_sql_active_record
9
+
10
+ def patch_rails?
11
+ !!defined?(::Rack::MiniProfiler::ENABLE_RAILS_PATCHES)
12
+ end
8
13
 
9
14
  def generate_id
10
15
  rand(36**20).to_s(36)
@@ -67,6 +72,17 @@ module Rack
67
72
  This feature is disabled by default, to enable set the enable_advanced_debugging_tools option to true in Mini Profiler config.
68
73
  TEXT
69
74
  end
75
+
76
+ def binds_to_params(binds)
77
+ return if binds.nil? || config.max_sql_param_length == 0
78
+ # map ActiveRecord::Relation::QueryAttribute to [name, value]
79
+ params = binds.map { |c| c.kind_of?(Array) ? [c.first, c.last] : [c.name, c.value] }
80
+ if (skip = config.skip_sql_param_names)
81
+ params.map { |(n, v)| n =~ skip ? [n, nil] : [n, v] }
82
+ else
83
+ params
84
+ end
85
+ end
70
86
  end
71
87
 
72
88
  #
@@ -6,6 +6,7 @@ module Rack
6
6
  # Timing system for a custom timers such as cache, redis, RPC, external API
7
7
  # calls, etc.
8
8
  class Custom < TimerStruct::Base
9
+ attr_accessor :parent
9
10
  def initialize(type, duration_ms, page, parent)
10
11
  @parent = parent
11
12
  @page = page
@@ -11,7 +11,7 @@ module Rack
11
11
  end
12
12
  end
13
13
 
14
- attr_accessor :children_duration
14
+ attr_accessor :children_duration, :start, :parent
15
15
 
16
16
  def initialize(name, page, parent)
17
17
  start_millis = (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i - page[:started]
@@ -62,10 +62,6 @@ module Rack
62
62
  self[:start_milliseconds]
63
63
  end
64
64
 
65
- def start
66
- @start
67
- end
68
-
69
65
  def depth
70
66
  self[:depth]
71
67
  end
@@ -91,6 +87,20 @@ module Rack
91
87
  end
92
88
  end
93
89
 
90
+ def move_child(child, destination)
91
+ if index = self[:children].index(child)
92
+ self[:children].slice!(index)
93
+ self[:has_children] = self[:children].size > 0
94
+
95
+ destination[:children].push(child)
96
+ destination[:has_children] = true
97
+
98
+ child[:parent_timing_id] = destination[:id]
99
+ child.parent = destination
100
+ child.adjust_depth
101
+ end
102
+ end
103
+
94
104
  def add_sql(query, elapsed_ms, page, params = nil, skip_backtrace = false, full_backtrace = false)
95
105
  TimerStruct::Sql.new(query, elapsed_ms, page, self, params, skip_backtrace, full_backtrace).tap do |timer|
96
106
  self[:sql_timings].push(timer)
@@ -102,6 +112,19 @@ module Rack
102
112
  end
103
113
  end
104
114
 
115
+ def move_sql(sql, destination)
116
+ if index = self[:sql_timings].index(sql)
117
+ self[:sql_timings].slice!(index)
118
+ self[:has_sql_timings] = self[:sql_timings].size > 0
119
+ self[:sql_timings_duration_milliseconds] -= sql[:duration_milliseconds]
120
+ destination[:sql_timings].push(sql)
121
+ destination[:has_sql_timings] = true
122
+ destination[:sql_timings_duration_milliseconds] += sql[:duration_milliseconds]
123
+ sql[:parent_timing_id] = destination[:id]
124
+ sql.parent = destination
125
+ end
126
+ end
127
+
105
128
  def add_custom(type, elapsed_ms, page)
106
129
  TimerStruct::Custom.new(type, elapsed_ms, page, self).tap do |timer|
107
130
  timer[:parent_timing_id] = self[:id]
@@ -119,18 +142,37 @@ module Rack
119
142
  end
120
143
  end
121
144
 
145
+ def move_custom(type, custom, destination)
146
+ if index = self[:custom_timings][type]&.index(custom)
147
+ custom[:parent_timing_id] = destination[:id]
148
+ custom.parent = destination
149
+ self[:custom_timings][type].slice!(index)
150
+ if self[:custom_timings][type].size == 0
151
+ self[:custom_timings].delete(type)
152
+ self[:custom_timing_stats].delete(type)
153
+ else
154
+ self[:custom_timing_stats][type][:count] -= 1
155
+ self[:custom_timing_stats][type][:duration] -= custom[:duration_milliseconds]
156
+ end
157
+ destination[:custom_timings][type] ||= []
158
+ destination[:custom_timings][type].push(custom)
159
+ destination[:custom_timing_stats][type] ||= { count: 0, duration: 0.0 }
160
+ destination[:custom_timing_stats][type][:count] += 1
161
+ destination[:custom_timing_stats][type][:duration] += custom[:duration_milliseconds]
162
+ end
163
+ end
164
+
122
165
  def record_time(milliseconds = nil)
123
166
  milliseconds ||= (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start) * 1000
124
167
  self[:duration_milliseconds] = milliseconds
125
168
  self[:is_trivial] = true if milliseconds < self[:trivial_duration_threshold_milliseconds]
126
- self[:duration_without_children_milliseconds] = milliseconds - @children_duration
127
-
128
- if @parent
129
- @parent.children_duration += milliseconds
130
- end
131
-
169
+ self[:duration_without_children_milliseconds] = milliseconds - self[:children].sum(&:duration_ms)
132
170
  end
133
171
 
172
+ def adjust_depth
173
+ self[:depth] = self.parent ? self.parent[:depth] + 1 : 0
174
+ self[:children].each(&:adjust_depth)
175
+ end
134
176
  end
135
177
  end
136
178
  end
@@ -6,6 +6,8 @@ module Rack
6
6
  # Timing system for a SQL query
7
7
  module TimerStruct
8
8
  class Sql < TimerStruct::Base
9
+ attr_accessor :parent
10
+
9
11
  def initialize(query, duration_ms, page, parent, params = nil, skip_backtrace = false, full_backtrace = false)
10
12
 
11
13
  stack_trace = nil
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  class MiniProfiler
5
- VERSION = '1.1.6'
5
+ VERSION = '2.0.0'
6
6
  end
7
7
  end
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'fileutils'
4
+ require_relative './railtie_methods'
4
5
 
5
6
  module Rack::MiniProfilerRails
7
+ extend Rack::MiniProfilerRailsMethods
6
8
 
7
9
  # call direct if needed to do a defer init
8
10
  def self.initialize!(app)
@@ -55,17 +57,81 @@ module Rack::MiniProfilerRails
55
57
 
56
58
  # Install the Middleware
57
59
  app.middleware.insert(0, Rack::MiniProfiler)
60
+ c.enable_advanced_debugging_tools = Rails.env.development?
61
+
62
+ if ::Rack::MiniProfiler.patch_rails?
63
+ # Attach to various Rails methods
64
+ ActiveSupport.on_load(:action_controller) do
65
+ ::Rack::MiniProfiler.profile_method(ActionController::Base, :process) { |action| "Executing action: #{action}" }
66
+ end
67
+
68
+ ActiveSupport.on_load(:action_view) do
69
+ ::Rack::MiniProfiler.profile_method(ActionView::Template, :render) { |x, y| "Rendering: #{@virtual_path}" }
70
+ end
71
+ else
72
+ subscribe("start_processing.action_controller") do |name, start, finish, id, payload|
73
+ next if !should_measure?
74
+
75
+ current = Rack::MiniProfiler.current
76
+ description = "Executing action: #{payload[:action]}"
77
+ Thread.current[get_key(payload)] = current.current_timer
78
+ Rack::MiniProfiler.current.current_timer = current.current_timer.add_child(description)
79
+ end
80
+
81
+ subscribe("process_action.action_controller") do |name, start, finish, id, payload|
82
+ next if !should_measure?
83
+
84
+ key = get_key(payload)
85
+ parent_timer = Thread.current[key]
86
+ next if !parent_timer
87
+
88
+ Thread.current[key] = nil
89
+ Rack::MiniProfiler.current.current_timer.record_time
90
+ Rack::MiniProfiler.current.current_timer = parent_timer
91
+ end
92
+
93
+ subscribe("render_partial.action_view") do |name, start, finish, id, payload|
94
+ render_notification_handler(shorten_identifier(payload[:identifier]), finish, start)
95
+ end
58
96
 
59
- # Attach to various Rails methods
60
- ActiveSupport.on_load(:action_controller) do
61
- ::Rack::MiniProfiler.profile_method(ActionController::Base, :process) { |action| "Executing action: #{action}" }
97
+ subscribe("render_template.action_view") do |name, start, finish, id, payload|
98
+ render_notification_handler(shorten_identifier(payload[:identifier]), finish, start)
99
+ end
100
+
101
+ if Rack::MiniProfiler.subscribe_sql_active_record
102
+ # we don't want to subscribe if we've already patched a DB driver
103
+ # otherwise we would end up with 2 records for every query
104
+ subscribe("sql.active_record") do |name, start, finish, id, payload|
105
+ next if !should_measure?
106
+ next if payload[:name] =~ /SCHEMA/ && Rack::MiniProfiler.config.skip_schema_queries
107
+
108
+ Rack::MiniProfiler.record_sql(
109
+ payload[:sql],
110
+ (finish - start) * 1000,
111
+ Rack::MiniProfiler.binds_to_params(payload[:binds])
112
+ )
113
+ end
114
+ end
62
115
  end
63
- ActiveSupport.on_load(:action_view) do
64
- ::Rack::MiniProfiler.profile_method(ActionView::Template, :render) { |x, y| "Rendering: #{@virtual_path}" }
116
+ @already_initialized = true
117
+ end
118
+
119
+ def self.subscribe(event, &blk)
120
+ if ActiveSupport::Notifications.respond_to?(:monotonic_subscribe)
121
+ ActiveSupport::Notifications.monotonic_subscribe(event) { |*args| blk.call(*args) }
122
+ else
123
+ ActiveSupport::Notifications.subscribe(event) do |name, start, finish, id, payload|
124
+ blk.call(name, start.to_f, finish.to_f, id, payload)
125
+ end
65
126
  end
127
+ end
66
128
 
67
- c.enable_advanced_debugging_tools = Rails.env.development?
68
- @already_initialized = true
129
+ def self.get_key(payload)
130
+ "mini_profiler_parent_timer_#{payload[:controller]}_#{payload[:action]}".to_sym
131
+ end
132
+
133
+ def self.shorten_identifier(identifier)
134
+ identifier.split('/').last(2).join('/')
69
135
  end
70
136
 
71
137
  def self.serves_static_assets?(app)
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack::MiniProfilerRailsMethods
4
+ def render_notification_handler(name, finish, start, name_as_description: false)
5
+ return if !should_measure?
6
+
7
+ description = name_as_description ? name : "Rendering: #{name}"
8
+ current = Rack::MiniProfiler.current.current_timer
9
+ node = current.add_child(description)
10
+ duration = finish - start
11
+ duration_ms = duration * 1000
12
+ node.start -= duration
13
+ node[:start_milliseconds] -= duration_ms
14
+ node.record_time(duration_ms)
15
+
16
+ children_duration = 0
17
+ to_be_moved = { requests: [], sql: [], custom: {} }
18
+ current.children.each do |child|
19
+ next if child == node
20
+ if should_move?(child, node)
21
+ to_be_moved[:requests] << child
22
+ children_duration += child[:duration_milliseconds]
23
+ end
24
+ end
25
+ node[:duration_without_children_milliseconds] = duration_ms - children_duration
26
+ to_be_moved[:requests].each { |req| current.move_child(req, node) }
27
+
28
+ current.sql_timings.each do |sql|
29
+ to_be_moved[:sql] << sql if should_move?(sql, node)
30
+ end
31
+ to_be_moved[:sql].each { |sql| current.move_sql(sql, node) }
32
+
33
+ current.custom_timings.each do |type, timings|
34
+ to_be_moved[:custom] = []
35
+ timings.each do |custom|
36
+ to_be_moved[:custom] << custom if should_move?(custom, node)
37
+ end
38
+ to_be_moved[:custom].each { |custom| current.move_custom(type, custom, node) }
39
+ end
40
+ end
41
+
42
+ def should_measure?
43
+ current = Rack::MiniProfiler.current
44
+ current && current.measure
45
+ end
46
+
47
+ def should_move?(child, node)
48
+ start = :start_milliseconds
49
+ duration = :duration_milliseconds
50
+ child[start] >= node[start] &&
51
+ child[start] + child[duration] <= node[start] + node[duration]
52
+ end
53
+
54
+ extend self
55
+ end
@@ -15,17 +15,6 @@ module Rack
15
15
  end
16
16
  end
17
17
 
18
- def binds_to_params(binds)
19
- return if binds.nil? || Rack::MiniProfiler.config.max_sql_param_length == 0
20
- # map ActiveRecord::Relation::QueryAttribute to [name, value]
21
- params = binds.map { |c| c.kind_of?(Array) ? [c.first, c.last] : [c.name, c.value] }
22
- if (skip = Rack::MiniProfiler.config.skip_sql_param_names)
23
- params.map { |(n, v)| n =~ skip ? [n, nil] : [n, v] }
24
- else
25
- params
26
- end
27
- end
28
-
29
18
  def log_with_miniprofiler(*args, &block)
30
19
  return log_without_miniprofiler(*args, &block) unless SqlPatches.should_measure?
31
20
 
@@ -37,7 +26,7 @@ module Rack
37
26
  return rval if Rack::MiniProfiler.config.skip_schema_queries && name =~ (/SCHEMA/)
38
27
 
39
28
  elapsed_time = SqlPatches.elapsed_time(start)
40
- Rack::MiniProfiler.record_sql(sql, elapsed_time, binds_to_params(binds))
29
+ Rack::MiniProfiler.record_sql(sql, elapsed_time, Rack::MiniProfiler.binds_to_params(binds))
41
30
  rval
42
31
  end
43
32
  end
@@ -1,16 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  if (defined?(Net) && defined?(Net::HTTP))
4
-
5
- Net::HTTP.class_eval do
6
- def request_with_mini_profiler(*args, &block)
7
- request = args[0]
4
+ module NetHTTPWithMiniProfiler
5
+ def request(request, *args, &block)
8
6
  Rack::MiniProfiler.step("Net::HTTP #{request.method} #{request.path}") do
9
- request_without_mini_profiler(*args, &block)
7
+ super
10
8
  end
11
9
  end
12
- alias request_without_mini_profiler request
13
- alias request request_with_mini_profiler
14
10
  end
15
11
 
12
+ Net::HTTP.prepend(NetHTTPWithMiniProfiler)
16
13
  end
@@ -23,18 +23,26 @@ class SqlPatches
23
23
  ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time).to_f * 1000).round(1)
24
24
  end
25
25
 
26
+ def self.patch_rails?
27
+ ::Rack::MiniProfiler.patch_rails?
28
+ end
29
+
26
30
  def self.sql_patches
27
31
  patches = []
28
32
 
29
33
  patches << 'mysql2' if defined?(Mysql2::Client) && Mysql2::Client.class == Class
30
34
  patches << 'pg' if defined?(PG::Result) && PG::Result.class == Class
31
35
  patches << 'oracle_enhanced' if defined?(ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter) && ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class == Class &&
32
- SqlPatches.correct_version?('~> 1.5.0', ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter)
36
+ SqlPatches.correct_version?('~> 1.5.0', ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter) &&
37
+ patch_rails?
33
38
  # if the adapters were directly patched, don't patch again
34
- return patches unless patches.empty?
39
+ if !patches.empty?
40
+ Rack::MiniProfiler.subscribe_sql_active_record = false
41
+ return patches
42
+ end
35
43
  patches << 'sequel' if defined?(Sequel::Database) && Sequel::Database.class == Class
36
- patches << 'activerecord' if defined?(ActiveRecord) && ActiveRecord.class == Module
37
-
44
+ patches << 'activerecord' if defined?(ActiveRecord) && ActiveRecord.class == Module && patch_rails?
45
+ Rack::MiniProfiler.subscribe_sql_active_record = patches.empty? && !patch_rails?
38
46
  patches
39
47
  end
40
48
 
@@ -27,7 +27,6 @@ require 'mini_profiler/context'
27
27
  require 'mini_profiler/client_settings'
28
28
  require 'mini_profiler/gc_profiler'
29
29
  require 'mini_profiler/profiler'
30
-
31
30
  require 'patches/sql_patches'
32
31
  require 'patches/net_patches'
33
32
 
@@ -11,7 +11,7 @@ Gem::Specification.new do |s|
11
11
  s.authors = ["Sam Saffron", "Robin Ward", "Aleks Totic"]
12
12
  s.description = "Profiling toolkit for Rack applications with Rails integration. Client Side profiling, DB profiling and Server profiling."
13
13
  s.email = "sam.saffron@gmail.com"
14
- s.homepage = "http://miniprofiler.com"
14
+ s.homepage = "https://miniprofiler.com"
15
15
  s.license = "MIT"
16
16
  s.files = [
17
17
  'rack-mini-profiler.gemspec',
@@ -39,6 +39,7 @@ Gem::Specification.new do |s|
39
39
  s.add_development_dependency 'rubocop'
40
40
  s.add_development_dependency 'mini_racer'
41
41
  s.add_development_dependency 'nokogiri'
42
+ s.add_development_dependency 'rubocop-discourse'
42
43
 
43
44
  s.require_paths = ["lib"]
44
45
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-mini-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.6
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-01-30 00:00:00.000000000 Z
13
+ date: 2020-03-11 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rack
@@ -180,6 +180,20 @@ dependencies:
180
180
  - - ">="
181
181
  - !ruby/object:Gem::Version
182
182
  version: '0'
183
+ - !ruby/object:Gem::Dependency
184
+ name: rubocop-discourse
185
+ requirement: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ type: :development
191
+ prerelease: false
192
+ version_requirements: !ruby/object:Gem::Requirement
193
+ requirements:
194
+ - - ">="
195
+ - !ruby/object:Gem::Version
196
+ version: '0'
183
197
  description: Profiling toolkit for Rack applications with Rails integration. Client
184
198
  Side profiling, DB profiling and Server profiling.
185
199
  email: sam.saffron@gmail.com
@@ -191,6 +205,7 @@ extra_rdoc_files:
191
205
  files:
192
206
  - CHANGELOG.md
193
207
  - README.md
208
+ - lib/enable_rails_patches.rb
194
209
  - lib/generators/rack_profiler/USAGE
195
210
  - lib/generators/rack_profiler/install_generator.rb
196
211
  - lib/generators/rack_profiler/templates/rack_profiler.rb
@@ -223,6 +238,7 @@ files:
223
238
  - lib/mini_profiler/timer_struct/sql.rb
224
239
  - lib/mini_profiler/version.rb
225
240
  - lib/mini_profiler_rails/railtie.rb
241
+ - lib/mini_profiler_rails/railtie_methods.rb
226
242
  - lib/patches/db/activerecord.rb
227
243
  - lib/patches/db/mongo.rb
228
244
  - lib/patches/db/moped.rb
@@ -239,7 +255,7 @@ files:
239
255
  - lib/patches/sql_patches.rb
240
256
  - lib/rack-mini-profiler.rb
241
257
  - rack-mini-profiler.gemspec
242
- homepage: http://miniprofiler.com
258
+ homepage: https://miniprofiler.com
243
259
  licenses:
244
260
  - MIT
245
261
  metadata: