local bench = script and require(script.Parent.bench_support) or require("bench_support") -- Copyright 2008 the V8 project authors. All rights reserved. -- Copyright 1996 John Maloney and Mario Wolczko. -- This program is free software; you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation; either version 2 of the License, or -- (at your option) any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU General Public License for more details. -- -- You should have received a copy of the GNU General Public License -- along with this program; if not, write to the Free Software -- Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -- This implementation of the DeltaBlue benchmark is derived -- from the Smalltalk implementation by John Maloney and Mario -- Wolczko. Some parts have been translated directly, whereas -- others have been modified more aggresively to make it feel -- more like a JavaScript program. -- -- A JavaScript implementation of the DeltaBlue constraint-solving -- algorithm, as described in: -- -- "The DeltaBlue Algorithm: An Incremental Constraint Hierarchy Solver" -- Bjorn N. Freeman-Benson and John Maloney -- January 1990 Communications of the ACM, -- also available as University of Washington TR 89-08-06. -- -- Beware: this benchmark is written in a grotesque style where -- the constraint model is built by side-effects from constructors. -- I've kept it this way to avoid deviating too much from the original -- implementation. -- function class(base) local T = {} T.__index = T if base then T.super = base setmetatable(T, base) end function T.new(...) local O = {} setmetatable(O, T) O:constructor(...) return O end return T end local planner --- O b j e c t M o d e l --- local function alert (...) print(...) end local OrderedCollection = class() function OrderedCollection:constructor() self.elms = {} end function OrderedCollection:add(elm) self.elms[#self.elms + 1] = elm end function OrderedCollection:at (index) return self.elms[index] end function OrderedCollection:size () return #self.elms end function OrderedCollection:removeFirst () local e = self.elms[#self.elms] self.elms[#self.elms] = nil return e end function OrderedCollection:remove (elm) local index = 0 local skipped = 0 for i = 1, #self.elms do local value = self.elms[i] if value ~= elm then self.elms[index] = value index = index + 1 else skipped = skipped + 1 end end local l = #self.elms for i = 1, skipped do self.elms[l - i + 1] = nil end end -- -- S t r e n g t h -- -- -- Strengths are used to measure the relative importance of constraints. -- New strengths may be inserted in the strength hierarchy without -- disrupting current constraints. Strengths cannot be created outside -- this class, so pointer comparison can be used for value comparison. -- local Strength = class() function Strength:constructor(strengthValue, name) self.strengthValue = strengthValue self.name = name end function Strength.stronger (s1, s2) return s1.strengthValue < s2.strengthValue end function Strength.weaker (s1, s2) return s1.strengthValue > s2.strengthValue end function Strength.weakestOf (s1, s2) return Strength.weaker(s1, s2) and s1 or s2 end function Strength.strongest (s1, s2) return Strength.stronger(s1, s2) and s1 or s2 end function Strength:nextWeaker () local v = self.strengthValue if v == 0 then return Strength.WEAKEST elseif v == 1 then return Strength.WEAK_DEFAULT elseif v == 2 then return Strength.NORMAL elseif v == 3 then return Strength.STRONG_DEFAULT elseif v == 4 then return Strength.PREFERRED elseif v == 5 then return Strength.REQUIRED end end -- Strength constants. Strength.REQUIRED = Strength.new(0, "required"); Strength.STONG_PREFERRED = Strength.new(1, "strongPreferred"); Strength.PREFERRED = Strength.new(2, "preferred"); Strength.STRONG_DEFAULT = Strength.new(3, "strongDefault"); Strength.NORMAL = Strength.new(4, "normal"); Strength.WEAK_DEFAULT = Strength.new(5, "weakDefault"); Strength.WEAKEST = Strength.new(6, "weakest"); -- -- C o n s t r a i n t -- -- -- An abstract class representing a system-maintainable relationship -- (or "constraint") between a set of variables. A constraint supplies -- a strength instance variable; concrete subclasses provide a means -- of storing the constrained variables and other information required -- to represent a constraint. -- local Constraint = class () function Constraint:constructor(strength) self.strength = strength end -- -- Activate this constraint and attempt to satisfy it. -- function Constraint:addConstraint () self:addToGraph() planner:incrementalAdd(self) end -- -- Attempt to find a way to enforce this constraint. If successful, -- record the solution, perhaps modifying the current dataflow -- graph. Answer the constraint that this constraint overrides, if -- there is one, or nil, if there isn't. -- Assume: I am not already satisfied. -- function Constraint:satisfy (mark) self:chooseMethod(mark) if not self:isSatisfied() then if self.strength == Strength.REQUIRED then alert("Could not satisfy a required constraint!") end return nil end self:markInputs(mark) local out = self:output() local overridden = out.determinedBy if overridden ~= nil then overridden:markUnsatisfied() end out.determinedBy = self if not planner:addPropagate(self, mark) then alert("Cycle encountered") end out.mark = mark return overridden end function Constraint:destroyConstraint () if self:isSatisfied() then planner:incrementalRemove(self) else self:removeFromGraph() end end -- -- Normal constraints are not input constraints. An input constraint -- is one that depends on external state, such as the mouse, the -- keybord, a clock, or some arbitraty piece of imperative code. -- function Constraint:isInput () return false end -- -- U n a r y C o n s t r a i n t -- -- -- Abstract superclass for constraints having a single possible output -- variable. -- local UnaryConstraint = class(Constraint) function UnaryConstraint:constructor (v, strength) UnaryConstraint.super.constructor(self, strength) self.myOutput = v self.satisfied = false self:addConstraint() end -- -- Adds this constraint to the constraint graph -- function UnaryConstraint:addToGraph () self.myOutput:addConstraint(self) self.satisfied = false end -- -- Decides if this constraint can be satisfied and records that -- decision. -- function UnaryConstraint:chooseMethod (mark) self.satisfied = (self.myOutput.mark ~= mark) and Strength.stronger(self.strength, self.myOutput.walkStrength); end -- -- Returns true if this constraint is satisfied in the current solution. -- function UnaryConstraint:isSatisfied () return self.satisfied; end function UnaryConstraint:markInputs (mark) -- has no inputs end -- -- Returns the current output variable. -- function UnaryConstraint:output () return self.myOutput end -- -- Calculate the walkabout strength, the stay flag, and, if it is -- 'stay', the value for the current output of this constraint. Assume -- this constraint is satisfied. -- function UnaryConstraint:recalculate () self.myOutput.walkStrength = self.strength self.myOutput.stay = not self:isInput() if self.myOutput.stay then self:execute() -- Stay optimization end end -- -- Records that this constraint is unsatisfied -- function UnaryConstraint:markUnsatisfied () self.satisfied = false end function UnaryConstraint:inputsKnown () return true end function UnaryConstraint:removeFromGraph () if self.myOutput ~= nil then self.myOutput:removeConstraint(self) end self.satisfied = false end -- -- S t a y C o n s t r a i n t -- -- -- Variables that should, with some level of preference, stay the same. -- Planners may exploit the fact that instances, if satisfied, will not -- change their output during plan execution. This is called "stay -- optimization". -- local StayConstraint = class(UnaryConstraint) function StayConstraint:constructor(v, str) StayConstraint.super.constructor(self, v, str) end function StayConstraint:execute () -- Stay constraints do nothing end -- -- E d i t C o n s t r a i n t -- -- -- A unary input constraint used to mark a variable that the client -- wishes to change. -- local EditConstraint = class (UnaryConstraint) function EditConstraint:constructor(v, str) EditConstraint.super.constructor(self, v, str) end -- -- Edits indicate that a variable is to be changed by imperative code. -- function EditConstraint:isInput () return true end function EditConstraint:execute () -- Edit constraints do nothing end -- -- B i n a r y C o n s t r a i n t -- local Direction = {} Direction.NONE = 0 Direction.FORWARD = 1 Direction.BACKWARD = -1 -- -- Abstract superclass for constraints having two possible output -- variables. -- local BinaryConstraint = class(Constraint) function BinaryConstraint:constructor(var1, var2, strength) BinaryConstraint.super.constructor(self, strength); self.v1 = var1 self.v2 = var2 self.direction = Direction.NONE self:addConstraint() end -- -- Decides if this constraint can be satisfied and which way it -- should flow based on the relative strength of the variables related, -- and record that decision. -- function BinaryConstraint:chooseMethod (mark) if self.v1.mark == mark then self.direction = (self.v2.mark ~= mark and Strength.stronger(self.strength, self.v2.walkStrength)) and Direction.FORWARD or Direction.NONE end if self.v2.mark == mark then self.direction = (self.v1.mark ~= mark and Strength.stronger(self.strength, self.v1.walkStrength)) and Direction.BACKWARD or Direction.NONE end if Strength.weaker(self.v1.walkStrength, self.v2.walkStrength) then self.direction = Strength.stronger(self.strength, self.v1.walkStrength) and Direction.BACKWARD or Direction.NONE else self.direction = Strength.stronger(self.strength, self.v2.walkStrength) and Direction.FORWARD or Direction.BACKWARD end end -- -- Add this constraint to the constraint graph -- function BinaryConstraint:addToGraph () self.v1:addConstraint(self) self.v2:addConstraint(self) self.direction = Direction.NONE end -- -- Answer true if this constraint is satisfied in the current solution. -- function BinaryConstraint:isSatisfied () return self.direction ~= Direction.NONE end -- -- Mark the input variable with the given mark. -- function BinaryConstraint:markInputs (mark) self:input().mark = mark end -- -- Returns the current input variable -- function BinaryConstraint:input () return (self.direction == Direction.FORWARD) and self.v1 or self.v2 end -- -- Returns the current output variable -- function BinaryConstraint:output () return (self.direction == Direction.FORWARD) and self.v2 or self.v1 end -- -- Calculate the walkabout strength, the stay flag, and, if it is -- 'stay', the value for the current output of this -- constraint. Assume this constraint is satisfied. -- function BinaryConstraint:recalculate () local ihn = self:input() local out = self:output() out.walkStrength = Strength.weakestOf(self.strength, ihn.walkStrength); out.stay = ihn.stay if out.stay then self:execute() end end -- -- Record the fact that self constraint is unsatisfied. -- function BinaryConstraint:markUnsatisfied () self.direction = Direction.NONE end function BinaryConstraint:inputsKnown (mark) local i = self:input() return i.mark == mark or i.stay or i.determinedBy == nil end function BinaryConstraint:removeFromGraph () if (self.v1 ~= nil) then self.v1:removeConstraint(self) end if (self.v2 ~= nil) then self.v2:removeConstraint(self) end self.direction = Direction.NONE end -- -- S c a l e C o n s t r a i n t -- -- -- Relates two variables by the linear scaling relationship: "v2 = -- (v1 * scale) + offset". Either v1 or v2 may be changed to maintain -- this relationship but the scale factor and offset are considered -- read-only. -- local ScaleConstraint = class (BinaryConstraint) function ScaleConstraint:constructor(src, scale, offset, dest, strength) self.direction = Direction.NONE self.scale = scale self.offset = offset ScaleConstraint.super.constructor(self, src, dest, strength) end -- -- Adds this constraint to the constraint graph. -- function ScaleConstraint:addToGraph () ScaleConstraint.super.addToGraph(self) self.scale:addConstraint(self) self.offset:addConstraint(self) end function ScaleConstraint:removeFromGraph () ScaleConstraint.super.removeFromGraph(self) if (self.scale ~= nil) then self.scale:removeConstraint(self) end if (self.offset ~= nil) then self.offset:removeConstraint(self) end end function ScaleConstraint:markInputs (mark) ScaleConstraint.super.markInputs(self, mark); self.offset.mark = mark self.scale.mark = mark end -- -- Enforce this constraint. Assume that it is satisfied. -- function ScaleConstraint:execute () if self.direction == Direction.FORWARD then self.v2.value = self.v1.value * self.scale.value + self.offset.value else self.v1.value = (self.v2.value - self.offset.value) / self.scale.value end end -- -- Calculate the walkabout strength, the stay flag, and, if it is -- 'stay', the value for the current output of this constraint. Assume -- this constraint is satisfied. -- function ScaleConstraint:recalculate () local ihn = self:input() local out = self:output() out.walkStrength = Strength.weakestOf(self.strength, ihn.walkStrength) out.stay = ihn.stay and self.scale.stay and self.offset.stay if out.stay then self:execute() end end -- -- E q u a l i t y C o n s t r a i n t -- -- -- Constrains two variables to have the same value. -- local EqualityConstraint = class (BinaryConstraint) function EqualityConstraint:constructor(var1, var2, strength) EqualityConstraint.super.constructor(self, var1, var2, strength) end -- -- Enforce this constraint. Assume that it is satisfied. -- function EqualityConstraint:execute () self:output().value = self:input().value end -- -- V a r i a b l e -- -- -- A constrained variable. In addition to its value, it maintain the -- structure of the constraint graph, the current dataflow graph, and -- various parameters of interest to the DeltaBlue incremental -- constraint solver. -- local Variable = class () function Variable:constructor(name, initialValue) self.value = initialValue or 0 self.constraints = OrderedCollection.new() self.determinedBy = nil self.mark = 0 self.walkStrength = Strength.WEAKEST self.stay = true self.name = name end -- -- Add the given constraint to the set of all constraints that refer -- this variable. -- function Variable:addConstraint (c) self.constraints:add(c) end -- -- Removes all traces of c from this variable. -- function Variable:removeConstraint (c) self.constraints:remove(c) if self.determinedBy == c then self.determinedBy = nil end end -- -- P l a n n e r -- -- -- The DeltaBlue planner -- local Planner = class() function Planner:constructor() self.currentMark = 0 end -- -- Attempt to satisfy the given constraint and, if successful, -- incrementally update the dataflow graph. Details: If satifying -- the constraint is successful, it may override a weaker constraint -- on its output. The algorithm attempts to resatisfy that -- constraint using some other method. This process is repeated -- until either a) it reaches a variable that was not previously -- determined by any constraint or b) it reaches a constraint that -- is too weak to be satisfied using any of its methods. The -- variables of constraints that have been processed are marked with -- a unique mark value so that we know where we've been. This allows -- the algorithm to avoid getting into an infinite loop even if the -- constraint graph has an inadvertent cycle. -- function Planner:incrementalAdd (c) local mark = self:newMark() local overridden = c:satisfy(mark) while overridden ~= nil do overridden = overridden:satisfy(mark) end end -- -- Entry point for retracting a constraint. Remove the given -- constraint and incrementally update the dataflow graph. -- Details: Retracting the given constraint may allow some currently -- unsatisfiable downstream constraint to be satisfied. We therefore collect -- a list of unsatisfied downstream constraints and attempt to -- satisfy each one in turn. This list is traversed by constraint -- strength, strongest first, as a heuristic for avoiding -- unnecessarily adding and then overriding weak constraints. -- Assume: c is satisfied. -- function Planner:incrementalRemove (c) local out = c:output() c:markUnsatisfied() c:removeFromGraph() local unsatisfied = self:removePropagateFrom(out) local strength = Strength.REQUIRED repeat for i = 1, unsatisfied:size() do local u = unsatisfied:at(i) if u.strength == strength then self:incrementalAdd(u) end end strength = strength:nextWeaker() until strength == Strength.WEAKEST end -- -- Select a previously unused mark value. -- function Planner:newMark () self.currentMark = self.currentMark + 1 return self.currentMark end -- -- Extract a plan for resatisfaction starting from the given source -- constraints, usually a set of input constraints. This method -- assumes that stay optimization is desired; the plan will contain -- only constraints whose output variables are not stay. Constraints -- that do no computation, such as stay and edit constraints, are -- not included in the plan. -- Details: The outputs of a constraint are marked when it is added -- to the plan under construction. A constraint may be appended to -- the plan when all its input variables are known. A variable is -- known if either a) the variable is marked (indicating that has -- been computed by a constraint appearing earlier in the plan), b) -- the variable is 'stay' (i.e. it is a constant at plan execution -- time), or c) the variable is not determined by any -- constraint. The last provision is for past states of history -- variables, which are not stay but which are also not computed by -- any constraint. -- Assume: sources are all satisfied. -- local Plan -- FORWARD DECLARATION function Planner:makePlan (sources) local mark = self:newMark() local plan = Plan.new() local todo = sources while todo:size() > 0 do local c = todo:removeFirst() if c:output().mark ~= mark and c:inputsKnown(mark) then plan:addConstraint(c) c:output().mark = mark self:addConstraintsConsumingTo(c:output(), todo) end end return plan end -- -- Extract a plan for resatisfying starting from the output of the -- given constraints, usually a set of input constraints. -- function Planner:extractPlanFromConstraints (constraints) local sources = OrderedCollection.new() for i = 1, constraints:size() do local c = constraints:at(i) if c:isInput() and c:isSatisfied() then -- not in plan already and eligible for inclusion sources:add(c) end end return self:makePlan(sources) end -- -- Recompute the walkabout strengths and stay flags of all variables -- downstream of the given constraint and recompute the actual -- values of all variables whose stay flag is true. If a cycle is -- detected, remove the given constraint and answer -- false. Otherwise, answer true. -- Details: Cycles are detected when a marked variable is -- encountered downstream of the given constraint. The sender is -- assumed to have marked the inputs of the given constraint with -- the given mark. Thus, encountering a marked node downstream of -- the output constraint means that there is a path from the -- constraint's output to one of its inputs. -- function Planner:addPropagate (c, mark) local todo = OrderedCollection.new() todo:add(c) while todo:size() > 0 do local d = todo:removeFirst() if d:output().mark == mark then self:incrementalRemove(c) return false end d:recalculate() self:addConstraintsConsumingTo(d:output(), todo) end return true end -- -- Update the walkabout strengths and stay flags of all variables -- downstream of the given constraint. Answer a collection of -- unsatisfied constraints sorted in order of decreasing strength. -- function Planner:removePropagateFrom (out) out.determinedBy = nil out.walkStrength = Strength.WEAKEST out.stay = true local unsatisfied = OrderedCollection.new() local todo = OrderedCollection.new() todo:add(out) while todo:size() > 0 do local v = todo:removeFirst() for i = 1, v.constraints:size() do local c = v.constraints:at(i) if not c:isSatisfied() then unsatisfied:add(c) end end local determining = v.determinedBy for i = 1, v.constraints:size() do local next = v.constraints:at(i); if next ~= determining and next:isSatisfied() then next:recalculate() todo:add(next:output()) end end end return unsatisfied end function Planner:addConstraintsConsumingTo (v, coll) local determining = v.determinedBy local cc = v.constraints for i = 1, cc:size() do local c = cc:at(i) if c ~= determining and c:isSatisfied() then coll:add(c) end end end -- -- P l a n -- -- -- A Plan is an ordered list of constraints to be executed in sequence -- to resatisfy all currently satisfiable constraints in the face of -- one or more changing inputs. -- Plan = class() function Plan:constructor() self.v = OrderedCollection.new() end function Plan:addConstraint (c) self.v:add(c) end function Plan:size () return self.v:size() end function Plan:constraintAt (index) return self.v:at(index) end function Plan:execute () for i = 1, self:size() do local c = self:constraintAt(i) c:execute() end end -- -- M a i 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. -- local function chainTest(n) planner = Planner.new() local prev = nil local first = nil local last = nil -- Build chain of n equality constraints for i = 0, n do local name = "v" .. i; local v = Variable.new(name) if prev ~= nil then EqualityConstraint.new(prev, v, Strength.REQUIRED) end if i == 0 then first = v end if i == n then last = v end prev = v end StayConstraint.new(last, Strength.STRONG_DEFAULT) local edit = EditConstraint.new(first, Strength.PREFERRED) local edits = OrderedCollection.new() edits:add(edit) local plan = planner:extractPlanFromConstraints(edits) for i = 0, 99 do first.value = i plan:execute() if last.value ~= i then alert("Chain test failed.") end end end local function change(v, newValue) local edit = EditConstraint.new(v, Strength.PREFERRED) local edits = OrderedCollection.new() edits:add(edit) local plan = planner:extractPlanFromConstraints(edits) for i = 1, 10 do v.value = newValue plan:execute() end edit:destroyConstraint() end -- -- 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. -- local function projectionTest(n) planner = Planner.new(); local scale = Variable.new("scale", 10); local offset = Variable.new("offset", 1000); local src = nil local dst = nil; local dests = OrderedCollection.new(); for i = 0, n - 1 do src = Variable.new("src" .. i, i); dst = Variable.new("dst" .. i, i); dests:add(dst); StayConstraint.new(src, Strength.NORMAL); ScaleConstraint.new(src, scale, offset, dst, Strength.REQUIRED); end change(src, 17) if dst.value ~= 1170 then alert("Projection 1 failed") end change(dst, 1050) if src.value ~= 5 then alert("Projection 2 failed") end change(scale, 5) for i = 0, n - 2 do if dests:at(i + 1).value ~= i * 5 + 1000 then alert("Projection 3 failed") end end change(offset, 2000) for i = 0, n - 2 do if dests:at(i + 1).value ~= i * 5 + 2000 then alert("Projection 4 failed") end end end function test() local t0 = os.clock() chainTest(1000); projectionTest(1000); local t1 = os.clock() return t1-t0 end bench.runCode(test, "deltablue")