Files
wren/test/benchmark/delta_blue.wren
2015-11-30 20:58:08 -08:00

691 lines
20 KiB
Plaintext

// Copyright 2011 Google Inc. All Rights Reserved.
// Copyright 1996 John Maloney and Mario Wolczko
//
// This file is part of GNU Smalltalk.
//
// GNU Smalltalk 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, or (at your option) any later version.
//
// GNU Smalltalk 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
// GNU Smalltalk; see the file COPYING. If not, write to the Free Software
// Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// Translated first from Smalltalk to JavaScript, and finally to
// Dart by Google 2008-2010.
//
// Translated to Wren by Bob Nystrom 2014.
// A Wren 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.
// 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 == can be used for value comparison.
class Strength {
construct new(value, name) {
_value = value
_name = name
}
value { _value }
name { _name }
nextWeaker { ORDERED[_value] }
static stronger(s1, s2) { s1.value < s2.value }
static weaker(s1, s2) { s1.value > s2.value }
static weakest(s1, s2) { Strength.weaker(s1, s2) ? s1 : s2 }
static strongest(s1, s2) { Strength.stronger(s1, s2) ? s1 : s2 }
}
// Compile time computed constants.
var REQUIRED = Strength.new(0, "required")
var STRONG_REFERRED = Strength.new(1, "strongPreferred")
var PREFERRED = Strength.new(2, "preferred")
var STRONG_DEFAULT = Strength.new(3, "strongDefault")
var NORMAL = Strength.new(4, "normal")
var WEAK_DEFAULT = Strength.new(5, "weakDefault")
var WEAKEST = Strength.new(6, "weakest")
var ORDERED = [
WEAKEST, WEAK_DEFAULT, NORMAL, STRONG_DEFAULT, PREFERRED, STRONG_REFERRED
]
var ThePlanner
class Constraint {
construct new(strength) {
_strength = strength
}
strength { _strength }
// Activate this constraint and attempt to satisfy it.
addConstraint() {
addToGraph()
ThePlanner.incrementalAdd(this)
}
// 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.
satisfy(mark) {
chooseMethod(mark)
if (!isSatisfied) {
if (_strength == REQUIRED) {
System.print("Could not satisfy a required constraint!")
}
return null
}
markInputs(mark)
var out = output
var overridden = out.determinedBy
if (overridden != null) overridden.markUnsatisfied()
out.determinedBy = this
if (!ThePlanner.addPropagate(this, mark)) System.print("Cycle encountered")
out.mark = mark
return overridden
}
destroyConstraint() {
if (isSatisfied) ThePlanner.incrementalRemove(this)
removeFromGraph()
}
// 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.
isInput { false }
}
// Abstract superclass for constraints having a single possible output variable.
class UnaryConstraint is Constraint {
construct new(myOutput, strength) {
super(strength)
_satisfied = false
_myOutput = myOutput
addConstraint()
}
// Adds this constraint to the constraint graph.
addToGraph() {
_myOutput.addConstraint(this)
_satisfied = false
}
// Decides if this constraint can be satisfied and records that decision.
chooseMethod(mark) {
_satisfied = (_myOutput.mark != mark) &&
Strength.stronger(strength, _myOutput.walkStrength)
}
// Returns true if this constraint is satisfied in the current solution.
isSatisfied { _satisfied }
markInputs(mark) {
// has no inputs.
}
// Returns the current output variable.
output { _myOutput }
// 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.
recalculate() {
_myOutput.walkStrength = strength
_myOutput.stay = !isInput
if (_myOutput.stay) execute() // Stay optimization.
}
// Records that this constraint is unsatisfied.
markUnsatisfied() {
_satisfied = false
}
inputsKnown(mark) { true }
removeFromGraph() {
if (_myOutput != null) _myOutput.removeConstraint(this)
_satisfied = false
}
}
// 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".
class StayConstraint is UnaryConstraint {
construct new(variable, strength) {
super(variable, strength)
}
execute() {
// Stay constraints do nothing.
}
}
// A unary input constraint used to mark a variable that the client
// wishes to change.
class EditConstraint is UnaryConstraint {
construct new(variable, strength) {
super(variable, strength)
}
// Edits indicate that a variable is to be changed by imperative code.
isInput { true }
execute() {
// Edit constraints do nothing.
}
}
// Directions.
var NONE = 1
var FORWARD = 2
var BACKWARD = 0
// Abstract superclass for constraints having two possible output
// variables.
class BinaryConstraint is Constraint {
construct new(v1, v2, strength) {
super(strength)
_v1 = v1
_v2 = v2
_direction = NONE
addConstraint()
}
direction { _direction }
v1 { _v1 }
v2 { _v2 }
// 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.
chooseMethod(mark) {
if (_v1.mark == mark) {
if (_v2.mark != mark &&
Strength.stronger(strength, _v2.walkStrength)) {
_direction = FORWARD
} else {
_direction = NONE
}
}
if (_v2.mark == mark) {
if (_v1.mark != mark &&
Strength.stronger(strength, _v1.walkStrength)) {
_direction = BACKWARD
} else {
_direction = NONE
}
}
if (Strength.weaker(_v1.walkStrength, _v2.walkStrength)) {
if (Strength.stronger(strength, _v1.walkStrength)) {
_direction = BACKWARD
} else {
_direction = NONE
}
} else {
if (Strength.stronger(strength, _v2.walkStrength)) {
_direction = FORWARD
} else {
_direction = BACKWARD
}
}
}
// Add this constraint to the constraint graph.
addToGraph() {
_v1.addConstraint(this)
_v2.addConstraint(this)
_direction = NONE
}
// Answer true if this constraint is satisfied in the current solution.
isSatisfied { _direction != NONE }
// Mark the input variable with the given mark.
markInputs(mark) {
input.mark = mark
}
// Returns the current input variable
input { _direction == FORWARD ? _v1 : _v2 }
// Returns the current output variable.
output { _direction == FORWARD ? _v2 : _v1 }
// 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.
recalculate() {
var ihn = input
var out = output
out.walkStrength = Strength.weakest(strength, ihn.walkStrength)
out.stay = ihn.stay
if (out.stay) execute()
}
// Record the fact that this constraint is unsatisfied.
markUnsatisfied() {
_direction = NONE
}
inputsKnown(mark) {
var i = input
return i.mark == mark || i.stay || i.determinedBy == null
}
removeFromGraph() {
if (_v1 != null) _v1.removeConstraint(this)
if (_v2 != null) _v2.removeConstraint(this)
_direction = NONE
}
}
// 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.
class ScaleConstraint is BinaryConstraint {
construct new(src, scale, offset, dest, strength) {
_scale = scale
_offset = offset
super(src, dest, strength)
}
// Adds this constraint to the constraint graph.
addToGraph() {
super()
_scale.addConstraint(this)
_offset.addConstraint(this)
}
removeFromGraph() {
super()
if (_scale != null) _scale.removeConstraint(this)
if (_offset != null) _offset.removeConstraint(this)
}
markInputs(mark) {
super.markInputs(mark)
_scale.mark = _offset.mark = mark
}
// Enforce this constraint. Assume that it is satisfied.
execute() {
if (direction == FORWARD) {
v2.value = v1.value * _scale.value + _offset.value
} else {
// TODO: Is this the same semantics as ~/?
v1.value = ((v2.value - _offset.value) / _scale.value).floor
}
}
// 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.
recalculate() {
var ihn = input
var out = output
out.walkStrength = Strength.weakest(strength, ihn.walkStrength)
out.stay = ihn.stay && _scale.stay && _offset.stay
if (out.stay) execute()
}
}
// Constrains two variables to have the same value.
class EqualityConstraint is BinaryConstraint {
construct new(v1, v2, strength) {
super(v1, v2, strength)
}
// Enforce this constraint. Assume that it is satisfied.
execute() {
output.value = input.value
}
}
// 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.
class Variable {
construct new(name, value) {
_constraints = []
_determinedBy = null
_mark = 0
_walkStrength = WEAKEST
_stay = true
_name = name
_value = value
}
constraints { _constraints }
determinedBy { _determinedBy }
determinedBy=(value) { _determinedBy = value }
mark { _mark }
mark=(value) { _mark = value }
walkStrength { _walkStrength }
walkStrength=(value) { _walkStrength = value }
stay { _stay }
stay=(value) { _stay = value }
value { _value }
value=(newValue) { _value = newValue }
// Add the given constraint to the set of all constraints that refer
// this variable.
addConstraint(constraint) {
_constraints.add(constraint)
}
// Removes all traces of c from this variable.
removeConstraint(constraint) {
_constraints = _constraints.where { |c| c != constraint }
if (_determinedBy == constraint) _determinedBy = null
}
}
// 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.
class Plan {
construct new() {
_list = []
}
addConstraint(constraint) {
_list.add(constraint)
}
size { _list.count }
execute() {
for (constraint in _list) {
constraint.execute()
}
}
}
class Planner {
construct new() {
_currentMark = 0
}
// 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.
incrementalAdd(constraint) {
var mark = newMark()
var overridden = constraint.satisfy(mark)
while (overridden != null) {
overridden = overridden.satisfy(mark)
}
}
// 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.
incrementalRemove(constraint) {
var out = constraint.output
constraint.markUnsatisfied()
constraint.removeFromGraph()
var unsatisfied = removePropagateFrom(out)
var strength = REQUIRED
while (true) {
for (u in unsatisfied) {
if (u.strength == strength) incrementalAdd(u)
}
strength = strength.nextWeaker
if (strength == WEAKEST) break
}
}
// Select a previously unused mark value.
newMark() { _currentMark = _currentMark + 1 }
// 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.
makePlan(sources) {
var mark = newMark()
var plan = Plan.new()
var todo = sources
while (todo.count > 0) {
var constraint = todo.removeAt(-1)
if (constraint.output.mark != mark && constraint.inputsKnown(mark)) {
plan.addConstraint(constraint)
constraint.output.mark = mark
addConstraintsConsumingTo(constraint.output, todo)
}
}
return plan
}
// Extract a plan for resatisfying starting from the output of the
// given [constraints], usually a set of input constraints.
extractPlanFromConstraints(constraints) {
var sources = []
for (constraint in constraints) {
// if not in plan already and eligible for inclusion.
if (constraint.isInput && constraint.isSatisfied) sources.add(constraint)
}
return makePlan(sources)
}
// 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.
addPropagate(constraint, mark) {
var todo = [constraint]
while (todo.count > 0) {
var d = todo.removeAt(-1)
if (d.output.mark == mark) {
incrementalRemove(constraint)
return false
}
d.recalculate()
addConstraintsConsumingTo(d.output, todo)
}
return true
}
// 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.
removePropagateFrom(out) {
out.determinedBy = null
out.walkStrength = WEAKEST
out.stay = true
var unsatisfied = []
var todo = [out]
while (todo.count > 0) {
var v = todo.removeAt(-1)
for (constraint in v.constraints) {
if (!constraint.isSatisfied) unsatisfied.add(constraint)
}
var determining = v.determinedBy
for (next in v.constraints) {
if (next != determining && next.isSatisfied) {
next.recalculate()
todo.add(next.output)
}
}
}
return unsatisfied
}
addConstraintsConsumingTo(v, coll) {
var determining = v.determinedBy
for (constraint in v.constraints) {
if (constraint != determining && constraint.isSatisfied) {
coll.add(constraint)
}
}
}
}
var total = 0
// 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.
var chainTest = Fn.new {|n|
ThePlanner = Planner.new()
var prev = null
var first = null
var last = null
// Build chain of n equality constraints.
for (i in 0..n) {
var v = Variable.new("v", 0)
if (prev != null) EqualityConstraint.new(prev, v, REQUIRED)
if (i == 0) first = v
if (i == n) last = v
prev = v
}
StayConstraint.new(last, STRONG_DEFAULT)
var edit = EditConstraint.new(first, PREFERRED)
var plan = ThePlanner.extractPlanFromConstraints([edit])
for (i in 0...100) {
first.value = i
plan.execute()
total = total + last.value
}
}
var change = Fn.new {|v, newValue|
var edit = EditConstraint.new(v, PREFERRED)
var plan = ThePlanner.extractPlanFromConstraints([edit])
for (i in 0...10) {
v.value = newValue
plan.execute()
}
edit.destroyConstraint()
}
// 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.
var projectionTest = Fn.new {|n|
ThePlanner = Planner.new()
var scale = Variable.new("scale", 10)
var offset = Variable.new("offset", 1000)
var src = null
var dst = null
var dests = []
for (i in 0...n) {
src = Variable.new("src", i)
dst = Variable.new("dst", i)
dests.add(dst)
StayConstraint.new(src, NORMAL)
ScaleConstraint.new(src, scale, offset, dst, REQUIRED)
}
change.call(src, 17)
total = total + dst.value
if (dst.value != 1170) System.print("Projection 1 failed")
change.call(dst, 1050)
total = total + src.value
if (src.value != 5) System.print("Projection 2 failed")
change.call(scale, 5)
for (i in 0...n - 1) {
total = total + dests[i].value
if (dests[i].value != i * 5 + 1000) System.print("Projection 3 failed")
}
change.call(offset, 2000)
for (i in 0...n - 1) {
total = total + dests[i].value
if (dests[i].value != i * 5 + 2000) System.print("Projection 4 failed")
}
}
var start = System.clock
for (i in 0...40) {
chainTest.call(100)
projectionTest.call(100)
}
System.print(total)
System.print("elapsed: %(System.clock - start)")