lab_tech 0.1.0
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +323 -0
- data/Rakefile +30 -0
- data/app/models/lab_tech/application_record.rb +5 -0
- data/app/models/lab_tech/default_cleaner.rb +87 -0
- data/app/models/lab_tech/experiment.rb +190 -0
- data/app/models/lab_tech/observation.rb +40 -0
- data/app/models/lab_tech/percentile.rb +41 -0
- data/app/models/lab_tech/result.rb +130 -0
- data/app/models/lab_tech/speedup.rb +65 -0
- data/app/models/lab_tech/summary.rb +183 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20190815192130_create_experiment_tables.rb +50 -0
- data/lib/lab_tech.rb +176 -0
- data/lib/lab_tech/engine.rb +6 -0
- data/lib/lab_tech/version.rb +3 -0
- data/lib/tasks/lab_tech_tasks.rake +4 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +1 -0
- data/spec/dummy/app/assets/javascripts/application.js +14 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +2 -0
- data/spec/dummy/app/jobs/application_job.rb +2 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +33 -0
- data/spec/dummy/bin/update +28 -0
- data/spec/dummy/config.ru +5 -0
- data/spec/dummy/config/application.rb +35 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +46 -0
- data/spec/dummy/config/environments/production.rb +71 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cors.rb +16 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +33 -0
- data/spec/dummy/config/puma.rb +34 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/spring.rb +6 -0
- data/spec/dummy/db/schema.rb +52 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +0 -0
- data/spec/dummy/log/test.log +1519 -0
- data/spec/examples.txt +79 -0
- data/spec/models/lab_tech/default_cleaner_spec.rb +32 -0
- data/spec/models/lab_tech/experiment_spec.rb +110 -0
- data/spec/models/lab_tech/percentile_spec.rb +85 -0
- data/spec/models/lab_tech/result_spec.rb +198 -0
- data/spec/models/lab_tech/speedup_spec.rb +133 -0
- data/spec/models/lab_tech/summary_spec.rb +325 -0
- data/spec/models/lab_tech_spec.rb +23 -0
- data/spec/rails_helper.rb +62 -0
- data/spec/spec_helper.rb +98 -0
- data/spec/support/misc_helpers.rb +7 -0
- metadata +238 -0
@@ -0,0 +1,183 @@
|
|
1
|
+
module LabTech
|
2
|
+
class Summary
|
3
|
+
TAB = " " * 4
|
4
|
+
LINE = "-" * 80
|
5
|
+
VAL = "█"
|
6
|
+
DOT = "·"
|
7
|
+
|
8
|
+
def initialize(experiment)
|
9
|
+
@experiment = experiment
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
if experiment.results.count.zero?
|
14
|
+
return [ LINE, "No results for experiment #{@experiment.name.inspect}", LINE ].join("\n")
|
15
|
+
end
|
16
|
+
|
17
|
+
fetch_data
|
18
|
+
|
19
|
+
s = StringIO.new
|
20
|
+
s.puts LINE, "Experiment: #{@experiment.name}", LINE
|
21
|
+
|
22
|
+
add_time_span_to s
|
23
|
+
add_counts_to s
|
24
|
+
|
25
|
+
if @time_deltas.any?
|
26
|
+
add_time_deltas_to s
|
27
|
+
add_speedup_chart_to s
|
28
|
+
end
|
29
|
+
|
30
|
+
s.puts LINE
|
31
|
+
return s.string
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :experiment
|
37
|
+
|
38
|
+
def add_counts_to(s)
|
39
|
+
s.puts
|
40
|
+
summarize_count( s, :correct )
|
41
|
+
summarize_count( s, :mismatched )
|
42
|
+
summarize_count( s, :timeout, "timed out" )
|
43
|
+
summarize_count( s, :errored, "raised errors" )
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_speedup_chart_to(s)
|
47
|
+
s.puts
|
48
|
+
s.puts "Speedups (by percentiles):"
|
49
|
+
speedup_magnitude = @speedup_factors.minmax.map(&:to_i).map(&:abs).max.ceil
|
50
|
+
speedup_magnitude = 25 if speedup_magnitude.zero?
|
51
|
+
(0..100).step(5) do |n|
|
52
|
+
s.puts TAB + speedup_summary_line(n, speedup_magnitude)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_time_deltas_to(s)
|
57
|
+
percentile = ->(n) { "%+.3fs" % LabTech::Percentile.call(n, @time_deltas) }
|
58
|
+
s.puts
|
59
|
+
s << "Median time delta: #{percentile.(50)}"
|
60
|
+
s << " "
|
61
|
+
s << "(90% of observations between #{percentile.(5)} and #{percentile.(95)})"
|
62
|
+
s.puts
|
63
|
+
end
|
64
|
+
|
65
|
+
def add_time_span_to(s)
|
66
|
+
t0, t1 = @earliest_result, @latest_result
|
67
|
+
s.puts "Earliest results: #{ t0.iso8601 }"
|
68
|
+
s.puts "Latest result: #{ t1.iso8601 } (%s)" \
|
69
|
+
% date_helper.distance_of_time_in_words(t0, t1)
|
70
|
+
end
|
71
|
+
|
72
|
+
def date_helper
|
73
|
+
@_date_helper ||= Object.new.tap do |o|
|
74
|
+
o.extend ActionView::Helpers::DateHelper
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def fetch_data
|
79
|
+
# Grab all aggregate operations counts/lists inside a transaction
|
80
|
+
# so all the counts are consistent
|
81
|
+
@experiment.transaction do
|
82
|
+
scope = experiment.results
|
83
|
+
|
84
|
+
@earliest_result = scope.minimum(:created_at)
|
85
|
+
@latest_result = scope.maximum(:created_at)
|
86
|
+
|
87
|
+
@counts = {
|
88
|
+
results: scope.count,
|
89
|
+
correct: scope.correct.count,
|
90
|
+
mismatched: scope.mismatched.count,
|
91
|
+
timeout: scope.timed_out.count,
|
92
|
+
errored: scope.other_error.count,
|
93
|
+
}
|
94
|
+
|
95
|
+
speedups = experiment.results.correct.pluck(:time_delta, :speedup_factor).map { |time, factor|
|
96
|
+
LabTech::Speedup.new(time: time, factor: factor)
|
97
|
+
}
|
98
|
+
@time_deltas = speedups.map(&:time).compact.sort
|
99
|
+
@speedup_factors = speedups.map(&:factor).compact.sort
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def highlight_bar(bar)
|
104
|
+
left, right = bar.split(VAL)
|
105
|
+
|
106
|
+
left = left .gsub(" ", " #{DOT}")
|
107
|
+
right = right.reverse.gsub(" ", " #{DOT}").reverse
|
108
|
+
|
109
|
+
left + VAL + right
|
110
|
+
end
|
111
|
+
|
112
|
+
def humanize(n)
|
113
|
+
width = number_helper.number_with_delimiter( @counts[:results] ).length
|
114
|
+
"%#{width}s" % number_helper.number_with_delimiter( n )
|
115
|
+
end
|
116
|
+
|
117
|
+
def pad_left(s, width)
|
118
|
+
n = [ ( width - s.length ), 0 ].max
|
119
|
+
[ " " * n , s ].join
|
120
|
+
end
|
121
|
+
|
122
|
+
def normalized_bar(x, magnitude, bar_scale: 25, highlight: false)
|
123
|
+
neg, pos = " " * bar_scale, " " * bar_scale
|
124
|
+
normalized = ( bar_scale * ( x.abs / magnitude ) ).floor
|
125
|
+
|
126
|
+
# Select an index that's as close to `normalized` as possible without generating IndexErrors
|
127
|
+
# (TODO: actually understand the math involved so I don't have to chop the ends off like an infidel)
|
128
|
+
index = [ 0, normalized ].max
|
129
|
+
index = [ index, bar_scale - 1 ].min
|
130
|
+
|
131
|
+
case
|
132
|
+
when x == 0 ; mid = VAL
|
133
|
+
when x < 0 ; mid = DOT ; neg[ index ] = VAL ; neg = neg.reverse
|
134
|
+
when x > 0 ; mid = DOT ; pos[ index ] = VAL
|
135
|
+
end
|
136
|
+
|
137
|
+
bar = "[%s%s%s]" % [ neg, mid, pos ]
|
138
|
+
bar = highlight_bar(bar) if highlight
|
139
|
+
bar
|
140
|
+
end
|
141
|
+
|
142
|
+
def number_helper
|
143
|
+
@_number_helper ||= Object.new.tap {|o| o.send :extend, ActionView::Helpers::NumberHelper }
|
144
|
+
end
|
145
|
+
|
146
|
+
def rate(n)
|
147
|
+
"%2.2f%%" % ( 100.0 * n / @counts[:results] )
|
148
|
+
end
|
149
|
+
|
150
|
+
def speedup_summary_line(n, speedup_magnitude)
|
151
|
+
highlight = n == 50
|
152
|
+
label = "%3d%%" % n
|
153
|
+
|
154
|
+
speedup_factor = LabTech::Percentile.call(n, @speedup_factors)
|
155
|
+
rel_speedup = "%+.1fx" % speedup_factor
|
156
|
+
bar = normalized_bar( speedup_factor, speedup_magnitude, highlight: highlight)
|
157
|
+
|
158
|
+
speedup_cue = pad_left( rel_speedup, speedup_width )
|
159
|
+
speedup_cue += " faster" if speedup_factor > 0
|
160
|
+
|
161
|
+
"#{label} #{bar} #{speedup_cue}"
|
162
|
+
end
|
163
|
+
|
164
|
+
def speedup_width
|
165
|
+
@_speedup_width ||= [
|
166
|
+
1, # sign
|
167
|
+
4, # digits
|
168
|
+
1, # decimal point
|
169
|
+
1, # digit after decimal point
|
170
|
+
].sum
|
171
|
+
end
|
172
|
+
|
173
|
+
def summarize_count(s, count_name, label = nil)
|
174
|
+
count = @counts[count_name]
|
175
|
+
return if count.zero?
|
176
|
+
|
177
|
+
total = @counts[:results]
|
178
|
+
label ||= count_name.to_s
|
179
|
+
s.puts "%s of %s (%s) %s" % [ humanize( count ), humanize( total ), rate( count ), label ]
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
class CreateExperimentTables < ActiveRecord::Migration[5.1]
|
2
|
+
def change
|
3
|
+
|
4
|
+
# Quick E-R diagram:
|
5
|
+
#
|
6
|
+
# +------------+ +--------+ +-------------+
|
7
|
+
# | Experiment |----E| Result |----E| Observation |
|
8
|
+
# +------------+ +--------+ +-------------+
|
9
|
+
|
10
|
+
create_table "lab_tech_experiments" do |t|
|
11
|
+
t.string "name"
|
12
|
+
t.integer "percent_enabled", null: false, default: 0
|
13
|
+
t.integer "equivalent_count", null: false, default: 0
|
14
|
+
t.integer "timed_out_count", null: false, default: 0
|
15
|
+
t.integer "other_error_count", null: false, default: 0
|
16
|
+
|
17
|
+
t.index [ "name" ], unique: true, name: "index_lab_tech_experiments_by_name"
|
18
|
+
end
|
19
|
+
|
20
|
+
create_table "lab_tech_results" do |t|
|
21
|
+
t.integer "experiment_id", null: false
|
22
|
+
t.text "context"
|
23
|
+
t.boolean "equivalent", null: false, default: false
|
24
|
+
t.boolean "raised_error", null: false, default: false
|
25
|
+
t.float "time_delta", limit: 24
|
26
|
+
t.float "speedup_factor", limit: 24
|
27
|
+
t.datetime "created_at"
|
28
|
+
t.boolean "timed_out", null: false, default: false
|
29
|
+
t.float "control_duration", limit: 24
|
30
|
+
t.float "candidate_duration", limit: 24
|
31
|
+
|
32
|
+
t.index [ "experiment_id", "equivalent" ], name: "index_lab_tech_results_by_exp_id_and_equivalent"
|
33
|
+
t.index [ "experiment_id", "raised_error" ], name: "index_lab_tech_results_by_exp_id_and_raised"
|
34
|
+
end
|
35
|
+
|
36
|
+
create_table "lab_tech_observations" do |t|
|
37
|
+
t.integer "result_id", null: false
|
38
|
+
t.string "name", limit: 100
|
39
|
+
t.float "duration", limit: 24
|
40
|
+
t.text "value", limit: 4294967295
|
41
|
+
t.text "sql"
|
42
|
+
t.string "exception_class"
|
43
|
+
t.text "exception_message"
|
44
|
+
t.text "exception_backtrace"
|
45
|
+
t.datetime "created_at"
|
46
|
+
|
47
|
+
t.index [ "result_id" ], name: "index_lab_tech_observations_by_result_id"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/lab_tech.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
require "lab_tech/engine"
|
2
|
+
require "scientist"
|
3
|
+
|
4
|
+
module LabTech
|
5
|
+
extend self
|
6
|
+
|
7
|
+
########################################################################
|
8
|
+
#
|
9
|
+
# So, you've come here for science? EXCELLENT.
|
10
|
+
#
|
11
|
+
# TL;DR:
|
12
|
+
#
|
13
|
+
# LabTech.science "experiment-name" do |exp|
|
14
|
+
# exp.use { STABLE_CODE } # this is the "control"
|
15
|
+
# exp.try { BETTER_CODE } # this is the "candidate"
|
16
|
+
#
|
17
|
+
# # Optional, but often useful:
|
18
|
+
# exp.context foo: "spam", bar: "eggs", yak: "bacon"
|
19
|
+
# exp.compare {|control, candidate| control.map(&:id) == candidate.map(&:id) }
|
20
|
+
# exp.clean { |records| records.map(&:id) }
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# See https://github.com/github/scientist for an *extremely* detailed
|
24
|
+
# README that explains how to use this. For those purposes, the thing
|
25
|
+
# passed to the block as `exp` is a Scientist::Experiment.
|
26
|
+
#
|
27
|
+
# NOTE: You'll probably want to check out the .enable and .disable methods
|
28
|
+
# below if you want your candidate code to actually *run*...
|
29
|
+
#
|
30
|
+
########################################################################
|
31
|
+
def science(experiment_name, opts = {}, &block)
|
32
|
+
experiment = Experiment.named( experiment_name )
|
33
|
+
|
34
|
+
yield experiment
|
35
|
+
|
36
|
+
test = opts[:run] if opts # TODO: figure out what this line was supposed to be for ¯\_(ツ)_/¯
|
37
|
+
experiment.run(test)
|
38
|
+
end
|
39
|
+
|
40
|
+
########################################################################
|
41
|
+
#
|
42
|
+
# This here is how you turn individual experiments on and off...
|
43
|
+
#
|
44
|
+
########################################################################
|
45
|
+
def self.enable(*experiment_names, percent: 100)
|
46
|
+
experiments_named( experiment_names ) do |exp|
|
47
|
+
exp.enable percent_enabled: percent
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.disable(*experiment_names)
|
52
|
+
experiments_named( experiment_names, &:disable )
|
53
|
+
end
|
54
|
+
|
55
|
+
########################################################################
|
56
|
+
#
|
57
|
+
# ...with an additional step if you want to record results in the Rails
|
58
|
+
# test environment.
|
59
|
+
#
|
60
|
+
########################################################################
|
61
|
+
def self.publish_results_in_test_mode? ; !!@publish_results_in_test_mode ; end
|
62
|
+
def self.publish_results_in_test_mode=(value) ; @publish_results_in_test_mode = !!value ; end
|
63
|
+
def self.publish_results_in_test_mode
|
64
|
+
fail ArgumentError, "a block is required for this method" unless block_given?
|
65
|
+
|
66
|
+
old_value = self.publish_results_in_test_mode?
|
67
|
+
self.publish_results_in_test_mode = true
|
68
|
+
yield
|
69
|
+
ensure
|
70
|
+
self.publish_results_in_test_mode = old_value
|
71
|
+
end
|
72
|
+
|
73
|
+
########################################################################
|
74
|
+
#
|
75
|
+
# You'll probably want to see how your experiments are doing...
|
76
|
+
#
|
77
|
+
########################################################################
|
78
|
+
def self.summarize_results(*experiment_names)
|
79
|
+
experiments_named( experiment_names, &:summarize_results )
|
80
|
+
end
|
81
|
+
|
82
|
+
########################################################################
|
83
|
+
#
|
84
|
+
# ...and be annoyed when they're not 100% correct...
|
85
|
+
#
|
86
|
+
########################################################################
|
87
|
+
#
|
88
|
+
# By default, this will simply print the values of all mismatches.
|
89
|
+
# However, if you'd like to pass a block that returns arguments to
|
90
|
+
# IO#puts, you can probably get more useful results.
|
91
|
+
#
|
92
|
+
# Here's one example based on an experiment that records the IDs
|
93
|
+
# returned from a search:
|
94
|
+
#
|
95
|
+
# comparison = ->(cont, cand) {
|
96
|
+
# cont_ids, cand_ids = cont.value, cand.value
|
97
|
+
# case
|
98
|
+
# when cont_ids == cand_ids ; "EVERYTHING IS FINE" # if this were true, it wouldn't be a mismatch
|
99
|
+
# when cont_ids.sort == cand_ids.sort ; "ORDER DIFFERS"
|
100
|
+
# else
|
101
|
+
# [
|
102
|
+
# "CONTROL length: #{ cont_ids.length }",
|
103
|
+
# "CANDIDATE length: #{ cand_ids.length }",
|
104
|
+
# " missing: #{ (cont_ids - cand_ids).inspect }",
|
105
|
+
# " extra: #{ (cand_ids - cont_ids).inspect }",
|
106
|
+
# ]
|
107
|
+
# end
|
108
|
+
# }
|
109
|
+
# e = Experiment.named "isolate-lead-activities-in-lead-search"
|
110
|
+
# e.compare_mismatches limit: 10, &comparison
|
111
|
+
#
|
112
|
+
# And here's another one that assumes you've recorded a hash of the form:
|
113
|
+
# { ids: [ 1, 2, ... ], sql: "SELECT FROM ..." }
|
114
|
+
#
|
115
|
+
# comparison = ->(cont, cand) {
|
116
|
+
# cont_ids, cand_ids = cont.value.fetch(:ids), cand.value.fetch(:ids)
|
117
|
+
# cont_sql, cand_sql = cont.value.fetch(:sql), cand.value.fetch(:sql)
|
118
|
+
# sql_strings = [ "", "CONTROL SQL", cont_sql, "", "CANDIDATE SQL", cand_sql ]
|
119
|
+
# case
|
120
|
+
# when cont_ids == cand_ids ; "EVERYTHING IS FINE" # if this were true, it wouldn't be a mismatch
|
121
|
+
# when cont_ids.sort == cand_ids.sort ; [ "ORDER DIFFERS" ] + sql_strings
|
122
|
+
# else
|
123
|
+
# [
|
124
|
+
# "CONTROL length: #{ cont_ids.length }",
|
125
|
+
# "CANDIDATE length: #{ cand_ids.length }",
|
126
|
+
# " missing: #{ (cont_ids - cand_ids).inspect }",
|
127
|
+
# " extra: #{ (cand_ids - cont_ids).inspect }",
|
128
|
+
# ] + sql_strings
|
129
|
+
# end
|
130
|
+
# }
|
131
|
+
# e = Experiment.named "isolate-lead-activities-in-lead-search"
|
132
|
+
# e.compare_mismatches limit: 10, &comparison
|
133
|
+
#
|
134
|
+
########################################################################
|
135
|
+
def self.compare_mismatches(experiment_name, limit: nil, io: $stdout, &block)
|
136
|
+
exp = LabTech::Experiment.named( experiment_name )
|
137
|
+
exp.compare_mismatches limit: limit, io: io, &block
|
138
|
+
end
|
139
|
+
|
140
|
+
########################################################################
|
141
|
+
#
|
142
|
+
# ...and be curious about the errors...
|
143
|
+
#
|
144
|
+
########################################################################
|
145
|
+
def self.summarize_errors(experiment_name, limit: nil, io: $stdout)
|
146
|
+
exp = LabTech::Experiment.named( experiment_name )
|
147
|
+
exp.summarize_errors( limit: limit, io: io )
|
148
|
+
end
|
149
|
+
|
150
|
+
########################################################################
|
151
|
+
#
|
152
|
+
# Sometimes specs might want to see that an experiment ran, the silly paranoid things
|
153
|
+
#
|
154
|
+
########################################################################
|
155
|
+
def self.reset_run_count!
|
156
|
+
run_count.clear
|
157
|
+
end
|
158
|
+
def self.run_count
|
159
|
+
@_experiment_run_count ||= Hash.new(0)
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
########################################################################
|
164
|
+
#
|
165
|
+
# Sometimes we want to act on a batch of experiments
|
166
|
+
# (this is mostly just plumbing; feel free to ignore it)
|
167
|
+
#
|
168
|
+
########################################################################
|
169
|
+
def self.experiments_named(*experiment_names, &block)
|
170
|
+
names = experiment_names.flatten.compact
|
171
|
+
names.each do |exp_name|
|
172
|
+
LabTech::Experiment.named(exp_name, &block)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
data/spec/dummy/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
//= link_directory ../stylesheets .css
|
@@ -0,0 +1,14 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file. JavaScript code in this file should be added after the last require_* statement.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require rails-ujs
|
14
|
+
//= require_tree .
|