lab_tech 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 .
|