nose 0.1.0pre

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.
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