luau/bench/tests/deltablue.lua

935 lines
25 KiB
Lua

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")