nose 0.1.0pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/lib/nose/backend/cassandra.rb +390 -0
  3. data/lib/nose/backend/file.rb +185 -0
  4. data/lib/nose/backend/mongo.rb +242 -0
  5. data/lib/nose/backend.rb +557 -0
  6. data/lib/nose/cost/cassandra.rb +33 -0
  7. data/lib/nose/cost/entity_count.rb +27 -0
  8. data/lib/nose/cost/field_size.rb +31 -0
  9. data/lib/nose/cost/request_count.rb +32 -0
  10. data/lib/nose/cost.rb +68 -0
  11. data/lib/nose/debug.rb +45 -0
  12. data/lib/nose/enumerator.rb +199 -0
  13. data/lib/nose/indexes.rb +239 -0
  14. data/lib/nose/loader/csv.rb +99 -0
  15. data/lib/nose/loader/mysql.rb +199 -0
  16. data/lib/nose/loader/random.rb +48 -0
  17. data/lib/nose/loader/sql.rb +105 -0
  18. data/lib/nose/loader.rb +38 -0
  19. data/lib/nose/model/entity.rb +136 -0
  20. data/lib/nose/model/fields.rb +293 -0
  21. data/lib/nose/model.rb +113 -0
  22. data/lib/nose/parser.rb +202 -0
  23. data/lib/nose/plans/execution_plan.rb +282 -0
  24. data/lib/nose/plans/filter.rb +99 -0
  25. data/lib/nose/plans/index_lookup.rb +302 -0
  26. data/lib/nose/plans/limit.rb +42 -0
  27. data/lib/nose/plans/query_planner.rb +361 -0
  28. data/lib/nose/plans/sort.rb +49 -0
  29. data/lib/nose/plans/update.rb +60 -0
  30. data/lib/nose/plans/update_planner.rb +270 -0
  31. data/lib/nose/plans.rb +135 -0
  32. data/lib/nose/proxy/mysql.rb +275 -0
  33. data/lib/nose/proxy.rb +102 -0
  34. data/lib/nose/query_graph.rb +481 -0
  35. data/lib/nose/random/barbasi_albert.rb +48 -0
  36. data/lib/nose/random/watts_strogatz.rb +50 -0
  37. data/lib/nose/random.rb +391 -0
  38. data/lib/nose/schema.rb +89 -0
  39. data/lib/nose/search/constraints.rb +143 -0
  40. data/lib/nose/search/problem.rb +328 -0
  41. data/lib/nose/search/results.rb +200 -0
  42. data/lib/nose/search.rb +266 -0
  43. data/lib/nose/serialize.rb +747 -0
  44. data/lib/nose/statements/connection.rb +160 -0
  45. data/lib/nose/statements/delete.rb +83 -0
  46. data/lib/nose/statements/insert.rb +146 -0
  47. data/lib/nose/statements/query.rb +161 -0
  48. data/lib/nose/statements/update.rb +101 -0
  49. data/lib/nose/statements.rb +645 -0
  50. data/lib/nose/timing.rb +79 -0
  51. data/lib/nose/util.rb +305 -0
  52. data/lib/nose/workload.rb +244 -0
  53. data/lib/nose.rb +37 -0
  54. data/templates/workload.erb +42 -0
  55. metadata +700 -0
@@ -0,0 +1,645 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoSE
4
+ # A single condition in a where clause
5
+ class Condition
6
+ attr_reader :field, :is_range, :operator, :value
7
+ alias range? is_range
8
+
9
+ def initialize(field, operator, value)
10
+ @field = field
11
+ @operator = operator
12
+ @is_range = [:>, :>=, :<, :<=].include? operator
13
+ @value = value
14
+
15
+ # XXX: Not frozen by now to support modification during query execution
16
+ # freeze
17
+ end
18
+
19
+ def inspect
20
+ "#{@field.inspect} #{@operator} #{value}"
21
+ end
22
+
23
+ # Compare conditions equal by their field and operator
24
+ # @return [Boolean]
25
+ def ==(other)
26
+ @field == other.field && @operator == other.operator
27
+ end
28
+ alias eql? ==
29
+
30
+ def hash
31
+ Zlib.crc32 [@field.id, @operator].to_s
32
+ end
33
+
34
+ # If the condition is on a foreign key, resolve
35
+ # it to the primary key of the related entity
36
+ # @return [Condition]
37
+ def resolve_foreign_key
38
+ return self unless field.is_a?(Fields::ForeignKeyField)
39
+
40
+ Condition.new @field.entity.id_field, @operator, @value
41
+ end
42
+ end
43
+
44
+ # Used to add a list of conditions to a {Statement}
45
+ module StatementConditions
46
+ attr_reader :conditions
47
+
48
+ # @return [void]
49
+ def populate_conditions(params)
50
+ @conditions = params[:conditions]
51
+ @eq_fields = conditions.each_value.reject(&:range?).map(&:field).to_set
52
+ @range_field = conditions.each_value.find(&:range?)
53
+ @range_field = @range_field.field unless @range_field.nil?
54
+ end
55
+
56
+ def self.included(base)
57
+ base.extend ClassMethods
58
+ end
59
+
60
+ # Add methods to the class for populating conditions
61
+ module ClassMethods
62
+ private
63
+
64
+ # Extract conditions from a parse tree
65
+ # @return [Hash]
66
+ def conditions_from_tree(tree, params)
67
+ conditions = tree[:where].nil? ? [] : tree[:where][:expression]
68
+ conditions = conditions.map { |c| build_condition c, tree, params }
69
+
70
+ params[:conditions] = Hash[conditions.map do |condition|
71
+ [condition.field.id, condition]
72
+ end]
73
+ end
74
+
75
+ # Construct a condition object from the parse tree
76
+ # @return [void]
77
+ def build_condition(condition, tree, params)
78
+ field = add_field_with_prefix tree[:path], condition[:field], params
79
+ Condition.new field, condition[:op].to_sym,
80
+ condition_value(condition, field)
81
+ end
82
+
83
+ # Get the value of a condition from the parse tree
84
+ # @return [Object]
85
+ def condition_value(condition, field)
86
+ value = condition[:value]
87
+
88
+ # Convert the value to the correct type
89
+ type = field.class.const_get 'TYPE'
90
+ value = field.class.value_from_string(value.to_s) \
91
+ unless type.nil? || value.nil?
92
+
93
+ # Don't allow predicates on foreign keys
94
+ fail InvalidStatementException, 'Predicates cannot use foreign keys' \
95
+ if field.is_a? Fields::ForeignKeyField
96
+
97
+ condition.delete :value
98
+
99
+ value
100
+ end
101
+ end
102
+ end
103
+
104
+ # A path from a primary key to a chain of foreign keys
105
+ class KeyPath
106
+ include Enumerable
107
+
108
+ extend Forwardable
109
+ def_delegators :@keys, :each, :inspect, :to_s, :length, :count, :last,
110
+ :empty?
111
+
112
+ def initialize(keys = [])
113
+ fail InvalidKeyPathException, 'first key must be an ID' \
114
+ unless keys.empty? || keys.first.instance_of?(Fields::IDField)
115
+
116
+ keys_match = keys.each_cons(2).all? do |prev_key, key|
117
+ key.parent == prev_key.entity
118
+ end
119
+ fail InvalidKeyPathException, 'keys must match along the path' \
120
+ unless keys_match
121
+
122
+ @keys = keys
123
+ end
124
+
125
+ # Two key paths are equal if their underlying keys are equal or the reverse
126
+ # @return [Boolean]
127
+ def ==(other, check_reverse = true)
128
+ @keys == other.instance_variable_get(:@keys) ||
129
+ (check_reverse && reverse.send(:==, other.reverse, false))
130
+ end
131
+ alias eql? ==
132
+
133
+ # Check if this path starts with another path
134
+ # @return [Boolean]
135
+ def start_with?(other, check_reverse = true)
136
+ other_keys = other.instance_variable_get(:@keys)
137
+ @keys[0..other_keys.length - 1] == other_keys ||
138
+ (check_reverse && reverse.start_with?(other.reverse, false))
139
+ end
140
+
141
+ # Check if a key is included in the path
142
+ # @return [Boolean]
143
+ def include?(key)
144
+ @keys.include?(key) || entities.any? { |e| e.id_field == key }
145
+ end
146
+
147
+ # Combine two key paths by gluing together the keys
148
+ # @return [KeyPath]
149
+ def +(other)
150
+ fail TypeError unless other.is_a? KeyPath
151
+ other_keys = other.instance_variable_get(:@keys)
152
+
153
+ # Just copy if there's no combining necessary
154
+ return dup if other_keys.empty?
155
+ return other.dup if @keys.empty?
156
+
157
+ # Only allow combining if the entities match
158
+ fail ArgumentError unless other_keys.first.parent == entities.last
159
+
160
+ # Combine the two paths
161
+ KeyPath.new(@keys + other_keys[1..-1])
162
+ end
163
+
164
+ # Return a slice of the path
165
+ # @return [KeyPath]
166
+ def [](index)
167
+ if index.is_a? Range
168
+ keys = @keys[index]
169
+ keys[0] = keys[0].entity.id_field \
170
+ unless keys.empty? || keys[0].instance_of?(Fields::IDField)
171
+ KeyPath.new(keys)
172
+ else
173
+ key = @keys[index]
174
+ key = key.entity.id_field \
175
+ unless key.nil? || key.instance_of?(Fields::IDField)
176
+ key
177
+ end
178
+ end
179
+
180
+ # Return the reverse of this path
181
+ # @return [KeyPath]
182
+ def reverse
183
+ KeyPath.new reverse_path
184
+ end
185
+
186
+ # Reverse this path in place
187
+ # @return [void]
188
+ def reverse!
189
+ @keys = reverse_path
190
+ end
191
+
192
+ # Simple wrapper so that we continue to be a KeyPath
193
+ # @return [KeyPath]
194
+ def to_a
195
+ self
196
+ end
197
+
198
+ # Return all the entities along the path
199
+ # @return [Array<Entity>]
200
+ def entities
201
+ @entities ||= @keys.map(&:entity)
202
+ end
203
+
204
+ # Split the path where it intersects the given entity
205
+ # @return [KeyPath]
206
+ def split(entity)
207
+ if first.parent == entity
208
+ query_keys = KeyPath.new([entity.id_field])
209
+ else
210
+ query_keys = []
211
+ each do |key|
212
+ query_keys << key
213
+ break if key.is_a?(Fields::ForeignKeyField) && key.entity == entity
214
+ end
215
+ query_keys = KeyPath.new(query_keys)
216
+ end
217
+ end
218
+
219
+ # Find where the path intersects the given
220
+ # entity and splice in the target path
221
+ # @return [KeyPath]
222
+ def splice(target, entity)
223
+ split(entity) + target
224
+ end
225
+
226
+ # Get the named path to reach this field through the list of keys
227
+ # @return [Array<String>]
228
+ def path_for_field(field)
229
+ return [field.name] if @keys.first.parent == field.parent
230
+
231
+ @keys.each_cons(2).take_while do |prev_key, _|
232
+ prev_key.entity != field.parent
233
+ end.map(&:last).map(&:name) << field.name
234
+ end
235
+
236
+ # Find the parent of a given field
237
+ # @Return [Entity]
238
+ def find_field_parent(field)
239
+ parent = find do |key|
240
+ field.parent == key.parent ||
241
+ (key.is_a?(Fields::ForeignKeyField) && field.parent == key.entity)
242
+ end
243
+
244
+ # This field is not on this portion of the path, so skip
245
+ return nil if parent.nil?
246
+
247
+ parent = parent.parent unless parent.is_a?(Fields::ForeignKeyField)
248
+ parent
249
+ end
250
+
251
+ # Produce all subpaths of this path
252
+ # @return [Enumerable<KeyPath>]
253
+ def subpaths(include_self = true)
254
+ Enumerator.new do |enum|
255
+ enum.yield self if include_self
256
+ 1.upto(@keys.length) do |i|
257
+ i.upto(@keys.length) do |j|
258
+ enum.yield self[i - 1..j - 1]
259
+ end
260
+ end
261
+ end
262
+ end
263
+
264
+ private
265
+
266
+ # Get the reverse path
267
+ # @return [Array<Fields::Field>]
268
+ def reverse_path
269
+ return [] if @keys.empty?
270
+ [@keys.last.entity.id_field] + @keys[1..-1].reverse.map(&:reverse)
271
+ end
272
+ end
273
+
274
+ # A CQL statement and its associated data
275
+ class Statement
276
+ attr_reader :entity, :key_path, :label, :graph,
277
+ :group, :text, :eq_fields, :range_field, :comment
278
+
279
+ # Parse either a query or an update
280
+ def self.parse(text, model, group: nil, label: nil, support: false)
281
+ klass = statement_class text, support
282
+ tree = parse_tree text, klass
283
+
284
+ # Ensure we have a valid path in the parse tree
285
+ tree[:path] ||= [tree[:entity]]
286
+ fail InvalidStatementException,
287
+ "FROM clause must start with #{tree[:entity]}" \
288
+ if tree[:entity] && tree[:path].first != tree[:entity]
289
+
290
+ params = statement_parameters tree, model
291
+ statement = klass.parse tree, params, text, group: group, label: label
292
+ statement.instance_variable_set :@comment, tree[:comment].to_s
293
+
294
+ # Support queries need to populate extra values before finalizing
295
+ unless support
296
+ statement.hash
297
+ statement.freeze
298
+ end
299
+
300
+ statement
301
+ end
302
+
303
+ # Produce the class of the statement for the given text
304
+ # @return [Class, Symbol]
305
+ def self.statement_class(text, support)
306
+ return SupportQuery if support
307
+
308
+ case text.split.first
309
+ when 'INSERT'
310
+ Insert
311
+ when 'DELETE'
312
+ Delete
313
+ when 'UPDATE'
314
+ Update
315
+ when 'CONNECT'
316
+ Connect
317
+ when 'DISCONNECT'
318
+ Disconnect
319
+ else # SELECT
320
+ Query
321
+ end
322
+ end
323
+ private_class_method :statement_class
324
+
325
+ # Run the parser and produce the parse tree
326
+ # @raise [ParseFailed]
327
+ # @return [Hash]
328
+ def self.parse_tree(text, klass)
329
+ # Set the type of the statement
330
+ # (but CONNECT and DISCONNECT use the same parse rule)
331
+ type = klass.name.split('::').last.downcase.to_sym
332
+ type = :connect if type == :disconnect
333
+
334
+ # If parsing fails, re-raise as our custom exception
335
+ begin
336
+ tree = CQLT.new.apply(CQLP.new.method(type).call.parse(text))
337
+ rescue Parslet::ParseFailed => exc
338
+ new_exc = ParseFailed.new exc.cause.ascii_tree
339
+ new_exc.set_backtrace exc.backtrace
340
+ raise new_exc
341
+ end
342
+
343
+ tree
344
+ end
345
+ private_class_method :parse_tree
346
+
347
+ # Produce the parameter hash needed to build a new statement
348
+ # @return [Hash]
349
+ def self.statement_parameters(tree, model)
350
+ entity = model[tree[:path].first.to_s]
351
+ key_path = find_longest_path(tree[:path], entity)
352
+
353
+ {
354
+ model: model,
355
+ entity: entity,
356
+ key_path: key_path,
357
+ graph: QueryGraph::Graph.from_path(key_path)
358
+ }
359
+ end
360
+ private_class_method :statement_parameters
361
+
362
+ # Calculate the longest path of entities traversed by the statement
363
+ # @return [KeyPath]
364
+ def self.find_longest_path(path_entities, from)
365
+ path = path_entities.map(&:to_s)[1..-1]
366
+ longest_entity_path = [from]
367
+ keys = [from.id_field]
368
+
369
+ path.each do |key|
370
+ # Search through foreign keys
371
+ last_entity = longest_entity_path.last
372
+ longest_entity_path << last_entity[key].entity
373
+ keys << last_entity[key]
374
+ end
375
+
376
+ KeyPath.new(keys)
377
+ end
378
+ private_class_method :find_longest_path
379
+
380
+ # A helper to look up a field based on the path specified in the statement
381
+ # @return [Fields::Field]
382
+ def self.add_field_with_prefix(path, field, params)
383
+ field_path = field.map(&:to_s)
384
+ prefix_index = path.index(field_path.first)
385
+ field_path = path[0..prefix_index - 1] + field_path \
386
+ unless prefix_index.zero?
387
+ field_path.map!(&:to_s)
388
+
389
+ # Expand the graph to include any keys which were found
390
+ field_path[0..-2].prefixes.drop(1).each do |key_path|
391
+ key = params[:model].find_field key_path
392
+ params[:graph].add_edge key.parent, key.entity, key
393
+ end
394
+
395
+ params[:model].find_field field_path
396
+ end
397
+ private_class_method :add_field_with_prefix
398
+
399
+ def initialize(params, text, group: nil, label: nil)
400
+ @entity = params[:entity]
401
+ @key_path = params[:key_path]
402
+ @longest_entity_path = @key_path.entities
403
+ @graph = params[:graph]
404
+ @model = params[:model]
405
+ @text = text
406
+ @group = group
407
+ @label = label
408
+ end
409
+
410
+ # Specifies if the statement modifies any data
411
+ # @return [Boolean]
412
+ def read_only?
413
+ false
414
+ end
415
+
416
+ # Specifies if the statement will require data to be inserted
417
+ # @return [Boolean]
418
+ def requires_insert?(_index)
419
+ false
420
+ end
421
+
422
+ # Specifies if the statement will require data to be deleted
423
+ # @return [Boolean]
424
+ def requires_delete?(_index)
425
+ false
426
+ end
427
+
428
+ # :nocov:
429
+ def to_color
430
+ "#{@text} [magenta]#{@longest_entity_path.map(&:name).join ', '}[/]"
431
+ end
432
+ # :nocov:
433
+
434
+ protected
435
+
436
+ # Quote the value of an identifier used as
437
+ # a value for a field, quoted if needed
438
+ # @return [String]
439
+ def maybe_quote(value, field)
440
+ if value.nil?
441
+ '?'
442
+ elsif [Fields::IDField,
443
+ Fields::ForeignKeyField,
444
+ Fields::StringField].include? field.class
445
+ "\"#{value}\""
446
+ else
447
+ value.to_s
448
+ end
449
+ end
450
+
451
+ # Generate a string which can be used in the "FROM" clause
452
+ # of a statement or optionally to specify a field
453
+ # @return [String]
454
+ def from_path(path, prefix_path = nil, field = nil)
455
+ if prefix_path.nil?
456
+ from = path.first.parent.name.dup
457
+ else
458
+ # Find where the two paths intersect to get the first path component
459
+ first_key = prefix_path.entries.find do |key|
460
+ path.entities.include?(key.parent) || \
461
+ key.is_a?(Fields::ForeignKeyField) && \
462
+ path.entities.include?(key.entity)
463
+ end
464
+ from = if first_key.primary_key?
465
+ first_key.parent.name.dup
466
+ else
467
+ first_key.name.dup
468
+ end
469
+ end
470
+
471
+ from << '.' << path.entries[1..-1].map(&:name).join('.') \
472
+ if path.length > 1
473
+
474
+ unless field.nil?
475
+ from << '.' unless from.empty?
476
+ from << field.name
477
+ end
478
+
479
+ from
480
+ end
481
+
482
+ # Produce a string which can be used
483
+ # as the settings clause in a statement
484
+ # @return [String]
485
+ def settings_clause
486
+ 'SET ' + @settings.map do |setting|
487
+ value = maybe_quote setting.value, setting.field
488
+ "#{setting.field.name} = #{value}"
489
+ end.join(', ')
490
+ end
491
+
492
+ # Produce a string which can be used
493
+ # as the WHERE clause in a statement
494
+ # @return [String]
495
+ def where_clause(field_namer = :to_s.to_proc)
496
+ ' WHERE ' + @conditions.values.map do |condition|
497
+ value = condition.value.nil? ? '?' : condition.value
498
+ "#{field_namer.call condition.field} #{condition.operator} #{value}"
499
+ end.join(' AND ')
500
+ end
501
+ end
502
+
503
+ # The setting of a field from an {Update} statement
504
+ class FieldSetting
505
+ attr_reader :field, :value
506
+
507
+ def initialize(field, value)
508
+ @field = field
509
+ @value = value
510
+
511
+ freeze
512
+ end
513
+
514
+ def inspect
515
+ "#{@field.inspect} = #{value}"
516
+ end
517
+
518
+ # Compare settings equal by their field
519
+ def ==(other)
520
+ other.field == @field
521
+ end
522
+ alias eql? ==
523
+
524
+ # Hash by field and value
525
+ def hash
526
+ Zlib.crc32 [@field.id, @value].to_s
527
+ end
528
+ end
529
+
530
+ # Module to add variable settings to a {Statement}
531
+ module StatementSettings
532
+ attr_reader :settings
533
+
534
+ def self.included(base)
535
+ base.extend ClassMethods
536
+ end
537
+
538
+ # Add methods to the class for populating settings
539
+ module ClassMethods
540
+ private
541
+
542
+ # Extract settings from a parse tree
543
+ # @return [Array<FieldSetting>]
544
+ def settings_from_tree(tree, params)
545
+ params[:settings] = tree[:settings].map do |setting|
546
+ field = params[:entity][setting[:field].to_s]
547
+ value = setting[:value]
548
+
549
+ type = field.class.const_get 'TYPE'
550
+ value = field.class.value_from_string(value.to_s) \
551
+ unless type.nil? || value.nil?
552
+
553
+ setting.delete :value
554
+ FieldSetting.new field, value
555
+ end
556
+ end
557
+ end
558
+ end
559
+
560
+ # Extend {Statement} objects to allow them to generate support queries
561
+ module StatementSupportQuery
562
+ # Determine if this statement modifies a particular index
563
+ def modifies_index?(index)
564
+ !(@settings.map(&:field).to_set & index.all_fields).empty?
565
+ end
566
+
567
+ # Support queries required to updating the given index with this statement
568
+ # @return [Array<SupportQuery>]
569
+ def support_queries(_index)
570
+ []
571
+ end
572
+
573
+ private
574
+
575
+ # Build a support query to update a given index
576
+ # and select fields with certain conditions
577
+ # @return [SupportQuery]
578
+ def build_support_query(entity, index, graph, select, conditions)
579
+ return nil if select.empty?
580
+
581
+ params = {
582
+ select: select,
583
+ graph: graph,
584
+ key_path: graph.longest_path,
585
+ entity: key_path.first.parent,
586
+ conditions: conditions
587
+ }
588
+
589
+ support_query = SupportQuery.new entity, params, nil, group: @group
590
+ support_query.instance_variable_set :@statement, self
591
+ support_query.instance_variable_set :@index, index
592
+ support_query.instance_variable_set :@comment, (hash ^ index.hash).to_s
593
+ support_query.instance_variable_set :@text, support_query.unparse
594
+ support_query.hash
595
+ support_query.freeze
596
+ end
597
+
598
+ # Produce support queries for the entity of the
599
+ # statement which select the given set of fields
600
+ # @return [Array<SupportQuery>]
601
+ def support_queries_for_entity(index, select)
602
+ graphs = index.graph.size > 1 ? index.graph.split(entity, true) : []
603
+
604
+ graphs.map do |graph|
605
+ support_fields = select.select do |field|
606
+ field.parent != entity && graph.entities.include?(field.parent)
607
+ end.to_set
608
+
609
+ conditions = {
610
+ entity.id_field.id => Condition.new(entity.id_field, :'=', nil)
611
+ }
612
+
613
+ split_entity = split_entity graph, index.graph, entity
614
+ build_support_query split_entity, index, graph, support_fields,
615
+ conditions
616
+ end.compact
617
+ end
618
+
619
+ # Determine which entity a subgraph was split at
620
+ # @return [Entity]
621
+ def split_entity(subgraph, graph, entity)
622
+ graph.keys_from_entity(entity).find do |key|
623
+ subgraph.entities.include? key.entity
624
+ end.entity
625
+ end
626
+ end
627
+
628
+ # Thrown when something tries to parse an invalid statement
629
+ class InvalidStatementException < StandardError
630
+ end
631
+
632
+ # Thrown when trying to construct a KeyPath which is not valid
633
+ class InvalidKeyPathException < StandardError
634
+ end
635
+
636
+ # Thrown when parsing a statement fails
637
+ class ParseFailed < StandardError
638
+ end
639
+ end
640
+
641
+ require_relative 'statements/connection'
642
+ require_relative 'statements/delete'
643
+ require_relative 'statements/insert'
644
+ require_relative 'statements/query'
645
+ require_relative 'statements/update'
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoSE
4
+ # Tracks the runtime of various functions and outputs a measurement
5
+ class Timer
6
+ # Start tracking function runtime
7
+ # @return [void]
8
+ def self.enable
9
+ traced = {
10
+ IndexEnumerator => [
11
+ :indexes_for_workload,
12
+ :support_indexes,
13
+ :combine_indexes
14
+ ],
15
+ Search::Search => [
16
+ :query_costs,
17
+ :update_costs,
18
+ :search_overlap,
19
+ :solve_mipper
20
+ ],
21
+ Search::Problem => [
22
+ :setup_model,
23
+ :add_variables,
24
+ :add_constraints,
25
+ :define_objective,
26
+ :total_cost,
27
+ :add_update_costs,
28
+ :total_size,
29
+ :total_indexes,
30
+ :solve
31
+ ],
32
+ MIPPeR::CbcModel => [
33
+ :add_constraints,
34
+ :add_variables,
35
+ :update,
36
+ :optimize
37
+ ]
38
+ }
39
+ @old_methods = Hash.new { |h, k| h[k] = {} }
40
+
41
+ # Redefine each method to capture timing information on each call
42
+ traced.each do |cls, methods|
43
+ methods.each do |method|
44
+ old_method = cls.instance_method(method)
45
+ cls.send(:define_method, method) do |*args|
46
+ $stderr.puts "#{cls}##{method}\tSTART"
47
+
48
+ start = Time.now.utc
49
+ result = old_method.bind(self).call(*args)
50
+ elapsed = Time.now.utc - start
51
+
52
+ # Allow a block to be called with the timing results
53
+ yield cls, method, elapsed if block_given?
54
+
55
+ $stderr.puts "#{cls}##{method}\tEND\t#{elapsed}"
56
+
57
+ result
58
+ end
59
+
60
+ # Save a copy of the old method for later
61
+ @old_methods[cls][method] = old_method
62
+ end
63
+ end
64
+ end
65
+
66
+ # Stop tracking function runtime
67
+ # @return [void]
68
+ def self.disable
69
+ @old_methods.each do |cls, methods|
70
+ methods.each do |method, old_method|
71
+ cls.send(:define_method, method, old_method)
72
+ end
73
+ end
74
+
75
+ # Remove the saved method definitions
76
+ @old_methods.clear
77
+ end
78
+ end
79
+ end