bud 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,18 @@
1
- == 0.9.2 / ???
1
+ == 0.9.3 / 2012-08-20
2
+
3
+ * Change behavior of accum() aggregate to return a Set, rather than an Array in
4
+ an unspecified order
5
+ * Fix several serious bugs in caching/invalidation of materialized operator
6
+ state (#276, #278, #279)
7
+ * Avoid possible spurious infinite loop with dbm-backed collections
8
+ * Optimize aggregation/grouping performance
9
+ * Fix bugs and improve performance for materialization of sort operator
10
+ * Fix REBL regression with push-based runtime (#274)
11
+ * Minor performance optimizations for simple projection rules
12
+ * Remove dependency on gchart
13
+ * Built-in support for code coverage with MRI 1.9 and SimpleCov
14
+
15
+ == 0.9.2 / 2012-05-19
2
16
 
3
17
  * Add new aggregate functions: bool_and() and bool_or()
4
18
  * Fix bugs in notin() stratification and implementation (#271)
@@ -16,7 +30,7 @@
16
30
  * Previous behavior was to ignore additional fields, but this was found to be
17
31
  error-prone
18
32
  * Remove builtin support for BUST (web services API); this avoids the need to
19
- depend on the json, nestful and i18n gems.
33
+ depend on the json, nestful and i18n gems
20
34
 
21
35
  == 0.9.0 / 2012-03-21
22
36
 
@@ -39,5 +53,4 @@
39
53
  * Support for Bloom-based signal handling has been removed
40
54
  * Support for the "with" syntax has been removed
41
55
  * The Bloom-based "deployment" framework has been removed
42
- * Support for Tokyo Cabinet-based collections has been removed
43
-
56
+ * Support for Tokyo Cabinet-backed collections has been removed
data/README.md CHANGED
@@ -37,6 +37,11 @@ To run the unit tests:
37
37
  % gem install minitest # unless already installed
38
38
  % cd test; ruby ts_bud.rb
39
39
 
40
+ To run the unit tests and produce a code coverage report:
41
+
42
+ % gem install simplecov # unless already installed
43
+ % cd test; COVERAGE=1 ruby ts_bud.rb
44
+
40
45
  ## Optional Dependencies
41
46
 
42
47
  The bud gem has a handful of mandatory dependencies. It also has one optional
@@ -239,7 +239,7 @@ Finally, we output every tuple of `bc` that does *not* appear in `t`.
239
239
  * `bc.group([:col1, :col2], min(:col3))`. *akin to min(col3) GROUP BY col1,col2*
240
240
  * exemplary aggs: `min`, `max`, `bool_and`, `bool_or`, `choose`
241
241
  * summary aggs: `sum`, `avg`, `count`
242
- * structural aggs: `accum`
242
+ * structural aggs: `accum` *accumulates inputs into a Set*
243
243
  * `bc.argmax([:attr1], :attr2)`      *returns the bc items per attr1 that have highest attr2*
244
244
  * `bc.argmin([:attr1], :attr2)`
245
245
  * `bc.argagg(:exemplary_agg_name, [:attr1], :attr2))`. *generalizes argmin/max: returns the bc items per attr1 that are chosen by the exemplary
@@ -14,7 +14,7 @@ This installs four things:
14
14
 
15
15
  * The `Bud` module, to embed Bloom code in Ruby.
16
16
  * The `rebl` executable: an interactive shell for trying out Bloom.
17
- * The `budplot` and `budvis` executables: graphical tools for visualizing and debugging Bloom programs.
17
+ * The `budplot`, `budvis`, and `budtimelines` executables: graphical tools for visualizing and debugging Bloom programs.
18
18
 
19
19
  ## First Blooms ##
20
20
 
data/lib/bud.rb CHANGED
@@ -315,12 +315,12 @@ module Bud
315
315
 
316
316
  # Check scan and merge_targets to see if any builtin_tables need to be added as well.
317
317
  @scanners.each do |scs|
318
- @app_tables += scs.values.map {|s| s.collection}
318
+ @app_tables.merge(scs.values.map {|s| s.collection})
319
319
  end
320
320
  @merge_targets.each do |mts| #mts == merge_targets at stratum
321
- @app_tables += mts
321
+ @app_tables.merge(mts)
322
322
  end
323
- @app_tables = @app_tables.nil? ? [] : @app_tables.to_a
323
+ @app_tables = @app_tables.to_a
324
324
 
325
325
  # for each stratum create a sorted list of push elements in topological order
326
326
  @push_sorted_elems = []
@@ -352,18 +352,18 @@ module Bud
352
352
  end
353
353
  end
354
354
 
355
- # sanity check
355
+ # Sanity check
356
356
  @push_sorted_elems.each do |stratum_elems|
357
- stratum_elems.each do |se|
358
- se.check_wiring
359
- end
357
+ stratum_elems.each {|se| se.check_wiring}
360
358
  end
361
359
 
362
- # create sets of elements and collections to invalidate or rescan at the beginning of each tick.
360
+ # Create sets of elements and collections to invalidate or rescan at the
361
+ # beginning of each tick
363
362
  prepare_invalidation_scheme
364
363
 
365
- # For all tables that are accessed (scanned) in a stratum higher than the one they are updated in, set
366
- # a flag to track deltas accumulated in that tick (see: collection.tick_delta)
364
+ # For all tables that are accessed (scanned) in a stratum higher than the
365
+ # one they are updated in, set a flag to track deltas accumulated in that
366
+ # tick (see: collection.tick_delta)
367
367
  stratum_accessed = {}
368
368
  (@num_strata-1).downto(0) do |stratum|
369
369
  @scanners[stratum].each_value do |s|
@@ -384,24 +384,38 @@ module Bud
384
384
  end
385
385
  end
386
386
 
387
- # All collections (elements included) are semantically required to erase any cached information at the start of a tick
388
- # and start from a clean slate. prepare_invalidation_scheme prepares a just-in-time invalidation scheme that
389
- # permits us to preserve data from one tick to the next, and to keep things in incremental mode unless there's a
390
- # negation.
387
+ # All collections (elements included) are semantically required to erase any
388
+ # cached information at the start of a tick and start from a clean
389
+ # slate. prepare_invalidation_scheme prepares a just-in-time invalidation
390
+ # scheme that permits us to preserve data from one tick to the next, and to
391
+ # keep things in incremental mode unless there's a negation.
392
+ #
391
393
  # This scheme solves the following constraints.
392
- # 1. A full scan of an element's contents results in downstream elements getting full scans themselves (i.e no \
393
- # deltas). This effect is transitive.
394
- # 2. Invalidation of an element's cache results in rebuilding of the cache and a consequent fullscan. See next.
395
- # 3. Invalidation of an element requires upstream elements to rescan their contents, or to transitively pass the
396
- # request on further upstream. Any element that has a cache can rescan without passing on the request to higher
397
- # levels.
398
394
  #
399
- # This set of constraints is solved once during wiring, resulting in four data structures
400
- # @default_invalidate = set of elements and tables to always invalidate at every tick. Organized by stratum
401
- # @default_rescan = set of elements and tables to always scan fully in the first iteration of every tick.
402
- # scanner[stratum].invalidate = Set of elements to additionally invalidate if the scanner's table is invalidated at
403
- # run-time
404
- # scanner[stratum].rescan = Similar to above.
395
+ # 1. A full scan of an element's contents results in downstream elements
396
+ # getting full scans themselves (i.e., no deltas). This effect is
397
+ # transitive.
398
+ # 2. Invalidation of an element's cache results in rebuilding of the cache and
399
+ # a consequent fullscan. See next.
400
+ # 3. Invalidation of an element requires upstream elements to rescan their
401
+ # contents, or to transitively pass the request on further upstream. Any
402
+ # element that has a cache can rescan without passing on the request to
403
+ # higher levels.
404
+ #
405
+ # This set of constraints is solved once during wiring, resulting in four data
406
+ # structures:
407
+ #
408
+ # @default_invalidate = Set of elements and tables to always invalidate at
409
+ # every tick.
410
+ #
411
+ # @default_rescan = Set of elements and tables to always scan fully in the
412
+ # first iteration of every tick.
413
+ #
414
+ # scanner[stratum].invalidate_set = Set of elements to additionally invalidate
415
+ # if the scanner's table is invalidated at
416
+ # run-time.
417
+ #
418
+ # scanner[stratum].rescan_set = Similar to above.
405
419
  def prepare_invalidation_scheme
406
420
  num_strata = @push_sorted_elems.size
407
421
  if $BUD_SAFE
@@ -443,22 +457,26 @@ module Bud
443
457
 
444
458
  num_strata.times do |stratum|
445
459
  @push_sorted_elems[stratum].each do |elem|
446
- if elem.rescan_at_tick
447
- rescan << elem
448
- end
460
+ rescan << elem if elem.rescan_at_tick
449
461
 
450
462
  if elem.outputs.any?{|tab| not(tab.class <= PushElement) and nm_targets.member? tab.qualified_tabname.to_sym }
451
- rescan += elem.wired_by
463
+ rescan.merge(elem.wired_by)
452
464
  end
453
465
  end
454
466
  rescan_invalidate_tc(stratum, rescan, invalidate)
455
467
  end
456
468
 
469
+ puts "(PRE) Default rescan: #{rescan.inspect}" if $BUD_DEBUG
470
+ puts "(PRE) Default inval: #{invalidate.inspect}" if $BUD_DEBUG
471
+
457
472
  prune_rescan_invalidate(rescan, invalidate)
458
473
  # transitive closure
459
474
  @default_rescan = rescan.to_a
460
475
  @default_invalidate = invalidate.to_a
461
476
 
477
+ puts "(POST) Default rescan: #{rescan.inspect}" if $BUD_DEBUG
478
+ puts "(POST) Default inval: #{invalidate.inspect}" if $BUD_DEBUG
479
+
462
480
  # Now compute for each table that is to be scanned, the set of dependent
463
481
  # tables and elements that will be invalidated if that table were to be
464
482
  # invalidated at run time.
@@ -475,7 +493,8 @@ module Bud
475
493
  invalidate = dflt_invalidate.clone
476
494
  rescan_invalidate_tc(stratum, rescan, invalidate)
477
495
  prune_rescan_invalidate(rescan, invalidate)
478
- to_reset += rescan + invalidate
496
+ to_reset.merge(rescan)
497
+ to_reset.merge(invalidate)
479
498
  # Give the diffs (from default) to scanner; these are elements that are
480
499
  # dependent on this scanner
481
500
  diffscan = (rescan - dflt_rescan).find_all {|elem| elem.class <= PushElement}
@@ -953,7 +972,7 @@ module Bud
953
972
  # One timestep of Bloom execution. This MUST be invoked from the EventMachine
954
973
  # thread; it is not intended to be called directly by client code.
955
974
  def tick_internal
956
- puts "#{object_id}/#{port} : =============================================" if $BUD_DEBUG
975
+ puts "#{object_id}/#{port} : ============================================= (#{@budtime})" if $BUD_DEBUG
957
976
  begin
958
977
  starttime = Time.now if options[:metrics]
959
978
  if options[:metrics] and not @endtime.nil?
@@ -979,7 +998,7 @@ module Bud
979
998
  num_strata = @push_sorted_elems.size
980
999
  # The following loop invalidates additional (non-default) elements and
981
1000
  # tables that depend on the run-time invalidation state of a table.
982
- # Loop once to set the flags
1001
+ # Loop once to set the flags.
983
1002
  num_strata.times do |stratum|
984
1003
  @scanners[stratum].each_value do |scanner|
985
1004
  if scanner.rescan
@@ -1034,7 +1053,6 @@ module Bud
1034
1053
  invoke_callbacks
1035
1054
  @budtime += 1
1036
1055
  @inbound.clear
1037
-
1038
1056
  @reset_list.each {|e| e.invalidated = false; e.rescan = false}
1039
1057
 
1040
1058
  ensure
@@ -1,3 +1,5 @@
1
+ require 'set'
2
+
1
3
  module Bud
2
4
  ######## Agg definitions
3
5
  class Agg #:nodoc: all
@@ -12,7 +14,7 @@ module Bud
12
14
  # b. :keep tells the caller to save this input
13
15
  # c. :replace tells the caller to keep this input alone
14
16
  # d. :delete, [t1, t2, ...] tells the caller to delete the remaining tuples
15
- # For things that do not descend from ArgExemplary, the 2nd part can simply be nil.
17
+ # For aggs that do not inherit from ArgExemplary, the 2nd part can simply be nil.
16
18
  def trans(the_state, val)
17
19
  return the_state, :ignore
18
20
  end
@@ -22,16 +24,12 @@ module Bud
22
24
  end
23
25
  end
24
26
 
25
- class Exemplary < Agg #:nodoc: all
26
- end
27
-
28
27
  # ArgExemplary aggs are used by argagg. Canonical examples are min/min (argmin/max)
29
28
  # They must have a trivial final method and be monotonic, i.e. once a value v
30
- # is discarded in favor of another, v can never be the final result
31
-
29
+ # is discarded in favor of another, v can never be the final result.
32
30
  class ArgExemplary < Agg #:nodoc: all
33
31
  def tie(the_state, val)
34
- (the_state == val)
32
+ the_state == val
35
33
  end
36
34
  def final(the_state)
37
35
  the_state
@@ -184,7 +182,7 @@ module Bud
184
182
  return retval, nil
185
183
  end
186
184
  def final(the_state)
187
- the_state[0]*1.0 / the_state[1]
185
+ the_state[0].to_f / the_state[1]
188
186
  end
189
187
  end
190
188
 
@@ -196,7 +194,7 @@ module Bud
196
194
 
197
195
  class Accum < Agg #:nodoc: all
198
196
  def init(x)
199
- [x]
197
+ [x].to_set
200
198
  end
201
199
  def trans(the_state, val)
202
200
  the_state << val
@@ -205,8 +203,7 @@ module Bud
205
203
  end
206
204
 
207
205
  # aggregate method to be used in Bud::BudCollection.group.
208
- # accumulates all x inputs into an array. note that the order of the elements
209
- # in the resulting array is undefined.
206
+ # accumulates all x inputs into a set.
210
207
  def accum(x)
211
208
  [Accum.new, x]
212
209
  end
@@ -23,10 +23,12 @@ class BudMeta #:nodoc: all
23
23
  # slot each rule into the stratum corresponding to its lhs pred (from stratum_map)
24
24
  stratified_rules = Array.new(top_stratum + 2) { [] } # stratum -> [ rules ]
25
25
  @bud_instance.t_rules.each do |rule|
26
- if rule.op.to_s == '<='
26
+ if rule.op == '<='
27
27
  # Deductive rules are assigned to strata based on the basic Datalog
28
28
  # stratification algorithm
29
29
  belongs_in = stratum_map[rule.lhs]
30
+ # If the rule body doesn't reference any collections, it won't be
31
+ # assigned a stratum, so just place it in stratum zero
30
32
  belongs_in ||= 0
31
33
  stratified_rules[belongs_in] << rule
32
34
  else
@@ -112,18 +114,16 @@ class BudMeta #:nodoc: all
112
114
  # a.b.c == s(:call, s1, :c, (:args))
113
115
  # where s1 == s(:call, s2, :b, (:args))
114
116
  # where s2 == s(:call, nil, :a, (:args))
115
-
116
117
  tag, recv, name, args = pt
117
- return nil unless tag == :call or args.length == 1
118
+ return nil unless tag == :call and args.length == 1
118
119
 
119
120
  if recv
120
121
  qn = get_qual_name(recv)
121
122
  return nil if qn.nil? or qn.size == 0
122
- qn = "#{qn}.#{name}"
123
+ return "#{qn}.#{name}"
123
124
  else
124
- qn = name.to_s
125
+ return name.to_s
125
126
  end
126
- qn
127
127
  end
128
128
 
129
129
  # Perform some basic sanity checks on the AST of a rule block. We expect a
@@ -156,9 +156,9 @@ class BudMeta #:nodoc: all
156
156
 
157
157
  # Check that LHS references a named collection
158
158
  lhs_name = get_qual_name(lhs)
159
- return [n, "Unexpected lhs format: #{lhs}"] if lhs.nil?
159
+ return [n, "unexpected lhs format: #{lhs}"] if lhs_name.nil?
160
160
  unless @bud_instance.tables.has_key? lhs_name.to_sym
161
- return [n, "Collection does not exist: '#{lhs_name}'"]
161
+ return [n, "collection does not exist: '#{lhs_name}'"]
162
162
  end
163
163
 
164
164
  return [n, "illegal operator: '#{op}'"] unless [:<, :<=].include? op
@@ -194,21 +194,21 @@ class BudMeta #:nodoc: all
194
194
  nodes = {}
195
195
  bud.t_depends.each do |d|
196
196
  #t_depends [:bud_instance, :rule_id, :lhs, :op, :body] => [:nm]
197
- lhs = (nodes[d.lhs.to_s] ||= Node.new(d.lhs.to_s, :init, 0, [], true, false, false, false))
197
+ lhs = (nodes[d.lhs] ||= Node.new(d.lhs, :init, 0, [], true, false, false, false))
198
198
  lhs.in_lhs = true
199
- body = (nodes[d.body.to_s] ||= Node.new(d.body.to_s, :init, 0, [], false, true, false, false))
199
+ body = (nodes[d.body] ||= Node.new(d.body, :init, 0, [], false, true, false, false))
200
200
  temporal = d.op != "<="
201
201
  lhs.edges << Edge.new(body, d.op, d.nm, temporal)
202
202
  body.in_body = true
203
203
  end
204
204
 
205
- nodes.values.each {|n| calc_stratum(n, false, false, [n.name])}
205
+ nodes.each_value {|n| calc_stratum(n, false, false, [n.name])}
206
206
  # Normalize stratum numbers because they may not be 0-based or consecutive
207
207
  remap = {}
208
208
  # if the nodes stratum numbers are [2, 3, 2, 4], remap = {2 => 0, 3 => 1, 4 => 2}
209
- nodes.values.map {|n| n.stratum}.uniq.sort.each_with_index{|num, i|
209
+ nodes.values.map {|n| n.stratum}.uniq.sort.each_with_index do |num, i|
210
210
  remap[num] = i
211
- }
211
+ end
212
212
  stratum_map = {}
213
213
  top_stratum = -1
214
214
  nodes.each_pair do |name, n|
@@ -232,9 +232,9 @@ class BudMeta #:nodoc: all
232
232
  node.status = :in_process
233
233
  node.edges.each do |edge|
234
234
  node.is_neg_head = edge.neg
235
- next if edge.op != "<="
235
+ next unless edge.op == "<="
236
236
  body_stratum = calc_stratum(edge.to, (neg or edge.neg), (edge.temporal or temporal), path + [edge.to.name])
237
- node.is_neg_head = false #reset for next edge
237
+ node.is_neg_head = false # reset for next edge
238
238
  node.stratum = max(node.stratum, body_stratum + (edge.neg ? 1 : 0))
239
239
  end
240
240
  node.status = :done
@@ -253,15 +253,15 @@ class BudMeta #:nodoc: all
253
253
  bud.t_provides.each do |p|
254
254
  pred, input = p.interface, p.input
255
255
  if input
256
- unless preds_in_body.include? pred.to_s
256
+ unless preds_in_body.include? pred
257
257
  # input interface is underspecified if not used in any rule body
258
258
  bud.t_underspecified << [pred, true] # true indicates input mode
259
259
  out.puts "Warning: input interface #{pred} not used"
260
260
  end
261
261
  else
262
- unless preds_in_lhs.include? pred.to_s
262
+ unless preds_in_lhs.include? pred
263
263
  # output interface underspecified if not in any rule's lhs
264
- bud.t_underspecified << [pred, false] #false indicates output mode.
264
+ bud.t_underspecified << [pred, false] # false indicates output mode
265
265
  out.puts "Warning: output interface #{pred} not used"
266
266
  end
267
267
  end
@@ -1,7 +1,6 @@
1
1
  require 'msgpack'
2
2
 
3
3
  $struct_classes = {}
4
- $EMPTY_HASH = {}
5
4
  module Bud
6
5
  ########
7
6
  #--
@@ -74,6 +73,10 @@ module Bud
74
73
  @qualified_tabname ||= @bud_instance.toplevel? ? tabname : "#{@bud_instance.qualified_name}.#{tabname}".to_sym
75
74
  end
76
75
 
76
+ def inspect
77
+ "#{self.class}:#{self.object_id.to_s(16)} [#{qualified_tabname}]"
78
+ end
79
+
77
80
  # The user-specified schema might come in two forms: a hash of Array =>
78
81
  # Array (key_cols => remaining columns), or simply an Array of columns (if
79
82
  # no key_cols were specified). Return a pair: [list of (all) columns, list
@@ -102,10 +105,6 @@ module Bud
102
105
  return [cols, key_cols]
103
106
  end
104
107
 
105
- def inspect
106
- "#{self.class}:#{self.object_id.to_s(16)} [#{qualified_tabname}]"
107
- end
108
-
109
108
  # produces the schema in a format that is useful as the schema specification for another table
110
109
  public
111
110
  def schema
@@ -189,6 +188,8 @@ module Bud
189
188
  def pro(the_name=tabname, the_schema=schema, &blk)
190
189
  if @bud_instance.wiring?
191
190
  pusher = to_push_elem(the_name, the_schema)
191
+ # If there is no code block evaluate, use the scanner directly
192
+ return pusher if blk.nil?
192
193
  pusher_pro = pusher.pro(&blk)
193
194
  pusher_pro.elem_name = the_name
194
195
  pusher_pro.tabname = the_name
@@ -260,7 +261,7 @@ module Bud
260
261
 
261
262
  public
262
263
  def non_temporal_predecessors
263
- @wired_by.map {|elem| elem if elem.outputs.include? self}
264
+ @wired_by.select {|elem| elem.outputs.include? self}
264
265
  end
265
266
 
266
267
  public
@@ -393,7 +394,7 @@ module Bud
393
394
 
394
395
  private
395
396
  def get_key_vals(t)
396
- @key_colnums.map {|i| t[i]}
397
+ t.values_at(*@key_colnums)
397
398
  end
398
399
 
399
400
  public
@@ -567,7 +568,7 @@ module Bud
567
568
  unless @delta.empty?
568
569
  puts "#{qualified_tabname}.tick_delta delta --> storage (#{@delta.size} elems)" if $BUD_DEBUG
569
570
  @storage.merge!(@delta)
570
- @tick_delta += @delta.values if accumulate_tick_deltas
571
+ @tick_delta.concat(@delta.values) if accumulate_tick_deltas
571
572
  @delta.clear
572
573
  end
573
574
 
@@ -607,7 +608,7 @@ module Bud
607
608
  end
608
609
  unless @delta.empty?
609
610
  @storage.merge!(@delta)
610
- @tick_delta += @delta.values if accumulate_tick_deltas
611
+ @tick_delta.concat(@delta.values) if accumulate_tick_deltas
611
612
  @delta.clear
612
613
  end
613
614
  unless @new_delta.empty?
@@ -704,14 +705,6 @@ module Bud
704
705
  return to_push_elem.reduce(initial, &blk)
705
706
  end
706
707
 
707
- public
708
- def pretty_print_instance_variables
709
- # list of attributes (in order) to print when pretty_print is called.
710
- important = ["@tabname", "@storage", "@delta", "@new_delta", "@pending"]
711
- # everything except bud_instance
712
- important + (self.instance_variables - important - ["@bud_instance"])
713
- end
714
-
715
708
  public
716
709
  def uniquify_tabname # :nodoc: all
717
710
  # just append current number of microseconds
@@ -747,7 +740,7 @@ module Bud
747
740
  srcs = non_temporal_predecessors
748
741
  if srcs.any? {|e| rescan.member? e}
749
742
  invalidate << self
750
- rescan += srcs
743
+ rescan.merge(srcs)
751
744
  end
752
745
  end
753
746
 
@@ -1145,7 +1138,7 @@ module Bud
1145
1138
  # No cache to invalidate. Also, tables do not invalidate dependents,
1146
1139
  # because their own state is not considered invalidated; that happens only
1147
1140
  # if there were pending deletes at the beginning of a tick (see tick())
1148
- puts "******** invalidate_cache called on BudTable"
1141
+ puts "******** invalidate_cache called on BudTable" if $BUD_DEBUG
1149
1142
  end
1150
1143
 
1151
1144
  public
@@ -1190,7 +1183,7 @@ module Bud
1190
1183
 
1191
1184
  public
1192
1185
  def each(&block)
1193
- @expr.call.each {|i| yield i}
1186
+ @expr.call.each(&block)
1194
1187
  end
1195
1188
 
1196
1189
  public
@@ -1221,7 +1214,7 @@ module Bud
1221
1214
 
1222
1215
  public
1223
1216
  def each(&blk)
1224
- each_raw {|l| blk.call(l)}
1217
+ each_raw(&blk)
1225
1218
  end
1226
1219
  end
1227
1220
  end