# Port of deltablue.py, as documented below, to Ruby. # Stefan Marr, 2014-04-28 # # Was: deltablue.py # ================= # # Ported for the PyPy project. # Contributed by Daniel Lindsley # # This implementation of the DeltaBlue benchmark was directly ported # from the `V8's source code`_, which was in turn derived # from the Smalltalk implementation by John Maloney and Mario # Wolczko. The original Javascript implementation was licensed under the GPL. # # It's been updated in places to be more idiomatic to Python (for loops over # collections, a couple magic methods, ``OrderedCollection`` being a list & things # altering those collections changed to the builtin methods) but largely retains # the layout & logic from the original. (Ugh.) # # .. _`V8's source code`: (http://code.google.com/p/v8/source/browse/branches/bleeding_edge/benchmarks/deltablue.js) # HOORAY FOR GLOBALS... Oh wait. # In spirit of the original, we'll keep it, but ugh. $planner = nil class Strength attr_accessor :strength def initialize(strength, name) @strength = strength @name = name end REQUIRED = self.new(0, "required") STRONG_PREFERRED = self.new(1, "strongPreferred") PREFERRED = self.new(2, "preferred") STRONG_DEFAULT = self.new(3, "strongDefault") NORMAL = self.new(4, "normal") WEAK_DEFAULT = self.new(5, "weakDefault") WEAKEST = self.new(6, "weakest") def self.stronger(s1, s2) s1.strength < s2.strength end def self.weaker(s1, s2) s1.strength > s2.strength end def self.weakest_of(s1, s2) if self.weaker(s1, s2) return s1 end return s2 end def self.strongest(s1, s2) if self.stronger(s1, s2) return s1 end return s2 end def next_weaker case @strength when 0 WEAKEST when 1 WEAK_DEFAULT when 2 NORMAL when 3 STRONG_DEFAULT when 4 PREFERRED when 5 # TODO: This looks like a bug in the original code. Shouldn't this be # ``STRONG_PREFERRED? Keeping for porting sake... REQUIRED end end end class Constraint attr_accessor :strength def initialize(strength) @strength = strength end def add_constraint add_to_graph $planner.incremental_add(self) end def satisfy(mark) choose_method(mark) if not is_satisfied if @strength == Strength::REQUIRED puts 'Could not satisfy a required constraint!' end return nil end mark_inputs(mark) out = output overridden = out.determined_by unless overridden.nil? overridden.mark_unsatisfied end out.determined_by = self unless $planner.add_propagate(self, mark) puts 'Cycle encountered' end out.mark = mark overridden end def destroy_constraint if is_satisfied $planner.incremental_remove(self) else remove_from_graph end end def is_input false end end class UnaryConstraint < Constraint def initialize(v, strength) super(strength) @my_output = v @satisfied = false add_constraint end def add_to_graph @my_output.add_constraint(self) @satisfied = false end def choose_method(mark) if @my_output.mark != mark and Strength.stronger(@strength, @my_output.walk_strength) @satisfied = true else @satisfied = false end end def is_satisfied @satisfied end def mark_inputs(mark) # No-ops. end def output # Ugh. Keeping it for consistency with the original. So much for # "we're all adults here"... @my_output end def recalculate @my_output.walk_strength = @strength @my_output.stay = !is_input if @my_output.stay execute end end def mark_unsatisfied @satisfied = false end def inputs_known(mark) true end def remove_from_graph unless @my_output.nil? @my_output.remove_constraint(self) @satisfied = false end end end class StayConstraint < UnaryConstraint def execute # The methods, THEY DO NOTHING. end end class EditConstraint < UnaryConstraint def is_input true end def execute # This constraint also does nothing. end end class Direction # Hooray for things that ought to be structs! NONE = 0 FORWARD = 1 BACKWARD = -1 end class BinaryConstraint < Constraint def initialize(v1, v2, strength) super(strength) @v1 = v1 @v2 = v2 @direction = Direction::NONE add_constraint end def choose_method(mark) if @v1.mark == mark if @v2.mark != mark and Strength::stronger(@strength, @v2.walk_strength) @direction = Direction::FORWARD else @direction = Direction::BACKWARD end end if @v2.mark == mark if @v1.mark != mark and Strength::stronger(@strength, @v1.walk_strength) @direction = Direction::BACKWARD else @direction = Direction::NONE end end if Strength.weaker(@v1.walk_strength, @v2.walk_strength) if Strength.stronger(@strength, @v1.walk_strength) @direction = Direction::BACKWARD else @direction = Direction::NONE end else if Strength.stronger(@strength, @v2.walk_strength) @direction = Direction::FORWARD else @direction = Direction::BACKWARD end end end def add_to_graph @v1.add_constraint(self) @v2.add_constraint(self) @direction = Direction::NONE end def is_satisfied @direction != Direction::NONE end def mark_inputs(mark) input.mark = mark end def input if @direction == Direction::FORWARD return @v1 end @v2 end def output if @direction == Direction::FORWARD return @v2 end @v1 end def recalculate ihn = input out = output out.walk_strength = Strength.weakest_of(@strength, ihn.walk_strength) out.stay = ihn.stay if out.stay execute end end def mark_unsatisfied @direction = Direction::NONE end def inputs_known(mark) i = input i.mark == mark or i.stay or i.determined_by.nil? end def remove_from_graph unless @v1.nil? @v1.remove_constraint(self) end unless @v2.nil? @v2.remove_constraint(self) end @direction = Direction::NONE end end class ScaleConstraint < BinaryConstraint def initialize(src, scale, offset, dest, strength) @direction = Direction::NONE @scale = scale @offset = offset super(src, dest, strength) end def add_to_graph super @scale.add_constraint(self) @offset.add_constraint(self) end def remove_from_graph super unless @scale.nil? @scale.remove_constraint(self) end unless @offset.nil? @offset.remove_constraint(self) end end def mark_inputs(mark) super @scale.mark = mark @offset.mark = mark end def execute if @direction == Direction::FORWARD @v2.value = @v1.value * @scale.value + @offset.value else @v1.value = (@v2.value - @offset.value) / @scale.value end end def recalculate ihn = input out = output out.walk_strength = Strength.weakest_of(@strength, ihn.walk_strength) out.stay = (ihn.stay and @scale.stay and @offset.stay) if out.stay execute end end end class EqualityConstraint < BinaryConstraint def execute output.value = input.value end end class Variable attr_accessor :mark attr_accessor :walk_strength attr_accessor :determined_by attr_accessor :stay attr_accessor :value attr_accessor :constraints def initialize(name, initial_value = 0) @name = name @value = initial_value @constraints = [] @determined_by = nil @mark = 0 @walk_strength = Strength::WEAKEST @stay = true end def inspect # To make debugging this beast from pdb easier... return "" end def add_constraint(constraint) @constraints << constraint end def remove_constraint(constraint) @constraints.delete(constraint) if @determined_by == constraint @determined_by = nil end end end class Planner def initialize @current_mark = 0 end def incremental_add(constraint) mark = new_mark overridden = constraint.satisfy(mark) until overridden.nil? overridden = overridden.satisfy(mark) end end def incremental_remove(constraint) out = constraint.output constraint.mark_unsatisfied constraint.remove_from_graph unsatisfied = remove_propagate_from(out) strength = Strength::REQUIRED loop do for u in unsatisfied if u.strength == strength incremental_add(u) end strength = strength.next_weaker end break if strength != Strength::WEAKEST end end def new_mark @current_mark += 1 @current_mark end def make_plan(sources) mark = new_mark plan = Plan.new() todo = sources until todo.empty? c = todo.pop if c.output.mark != mark and c.inputs_known(mark) plan.add_constraint(c) c.output.mark = mark add_constraints_consuming_to(c.output, todo) end end plan end def extract_plan_from_constraints(constraints) sources = [] for c in constraints if c.is_input and c.is_satisfied sources << c end end make_plan(sources) end def add_propagate(c, mark) todo = [] todo << c until todo.empty? d = todo.pop if d.output.mark == mark incremental_remove(c) return false end d.recalculate add_constraints_consuming_to(d.output, todo) end true end def remove_propagate_from(out) out.determined_by = nil out.walk_strength = Strength::WEAKEST out.stay = true unsatisfied = [] todo = [] todo << out until todo.empty? v = todo.pop for c in v.constraints unless c.is_satisfied unsatisfied << c end end determining = v.determined_by for c in v.constraints if c != determining and c.is_satisfied c.recalculate todo << c.output end end end unsatisfied end def add_constraints_consuming_to(v, coll) determining = v.determined_by cc = v.constraints for c in cc if c != determining and c.is_satisfied # I guess we're just updating a reference (``coll``)? Seems # inconsistent with the rest of the implementation, where they # return the lists... coll << c end end end end class Plan def initialize @v = [] end def add_constraint(c) @v << c end def size @v.size end def [](index) @v[index] end def execute for c in @v c.execute end end end # Main def chain_test(n) # This is the standard DeltaBlue benchmark. A long chain of equality # constraints is constructed with a stay constraint on one end. An # edit constraint is then added to the opposite end and the time is # measured for adding and removing this constraint, and extracting # and executing a constraint satisfaction plan. There are two cases. # In case 1, the added constraint is stronger than the stay # constraint and values must propagate down the entire length of the # chain. In case 2, the added constraint is weaker than the stay # constraint so it cannot be accomodated. The cost in this case is, # of course, very low. Typical situations lie somewhere between these # two extremes. $planner = Planner.new prev, first, last = nil, nil, nil # We need to go up to n inclusively. for i in 0..n #name = "v%s" % i name = sprintf "v%s", i v = Variable.new(name) unless prev.nil? EqualityConstraint.new(prev, v, Strength::REQUIRED) end if i == 0 first = v end if i == n last = v end prev = v end StayConstraint.new(last, Strength::STRONG_DEFAULT) edit = EditConstraint.new(first, Strength::PREFERRED) edits = [] edits << edit plan = $planner.extract_plan_from_constraints(edits) for i in 0..99 first.value = i plan.execute if last.value != i puts 'Chain test failed.' exit end end end def projection_test(n) # This test constructs a two sets of variables related to each # other by a simple linear transformation (scale and offset). The # time is measured to change a variable on either side of the # mapping and to change the scale and offset factors. $planner = Planner.new scale = Variable.new("scale", 10) offset = Variable.new("offset", 1000) src, dst = nil, nil dests = [] for i in 0..(n - 1) #src = Variable.new("src%s" % i, i) src = Variable.new(sprintf("src%s", i), i) #dst = Variable.new("dst%s" % i, i) dst = Variable.new(sprintf("dst%s", i), i) dests << dst StayConstraint.new(src, Strength::NORMAL) ScaleConstraint.new(src, scale, offset, dst, Strength::REQUIRED) end change(src, 17) if dst.value != 1170 puts 'Projection 1 failed' exit end change(dst, 1050) if src.value != 5 puts 'Projection 2 failed' exit end change(scale, 5) for i in 0..(n - 2) if dests[i].value != (i * 5 + 1000) puts 'Projection 3 failed' exit end end change(offset, 2000) for i in 0..(n - 2) if dests[i].value != (i * 5 + 2000) puts 'Projection 4 failed' exit end end end def change(v, new_value) edit = EditConstraint.new(v, Strength::PREFERRED) edits = [] edits << edit plan = $planner.extract_plan_from_constraints(edits) for i in 0..9 v.value = new_value plan.execute end edit.destroy_constraint end def run(iterations) chain_test(iterations) projection_test(iterations) end def harness_input 10_000 end def harness_sample(input) run(input) end def harness_verify(output) # Self-verifies true end require 'bench9000/harness'