forked from Mirror/wren
Mainly to get rid of one top level directory. But this will also be useful when there are tests of the embedding API.
637 lines
17 KiB
Python
637 lines
17 KiB
Python
"""
|
|
deltablue.py
|
|
============
|
|
|
|
Ported for the PyPy project.
|
|
|
|
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)
|
|
|
|
From: https://gist.github.com/toastdriven/6408132
|
|
|
|
I (Bob Nystrom) tweaked it a bit more. It now prints some output just to be
|
|
sure it's doing the same work, and I use normal lists instead of wrapping it in
|
|
OrderedCollection.
|
|
|
|
"""
|
|
from __future__ import print_function
|
|
import time
|
|
|
|
__author__ = 'Daniel Lindsley'
|
|
__license__ = 'BSD'
|
|
|
|
|
|
class Strength(object):
|
|
REQUIRED = None
|
|
STRONG_PREFERRED = None
|
|
PREFERRED = None
|
|
STRONG_DEFAULT = None
|
|
NORMAL = None
|
|
WEAK_DEFAULT = None
|
|
WEAKEST = None
|
|
|
|
def __init__(self, strength, name):
|
|
super(Strength, self).__init__()
|
|
self.strength = strength
|
|
self.name = name
|
|
|
|
@classmethod
|
|
def stronger(cls, s1, s2):
|
|
return s1.strength < s2.strength
|
|
|
|
@classmethod
|
|
def weaker(cls, s1, s2):
|
|
return s1.strength > s2.strength
|
|
|
|
@classmethod
|
|
def weakest_of(cls, s1, s2):
|
|
if cls.weaker(s1, s2):
|
|
return s1
|
|
|
|
return s2
|
|
|
|
@classmethod
|
|
def strongest(cls, s1, s2):
|
|
if cls.stronger(s1, s2):
|
|
return s1
|
|
|
|
return s2
|
|
|
|
def next_weaker(self):
|
|
strengths = {
|
|
0: self.__class__.WEAKEST,
|
|
1: self.__class__.WEAK_DEFAULT,
|
|
2: self.__class__.NORMAL,
|
|
3: self.__class__.STRONG_DEFAULT,
|
|
4: self.__class__.PREFERRED,
|
|
# TODO: This looks like a bug in the original code. Shouldn't this be
|
|
# ``STRONG_PREFERRED? Keeping for porting sake...
|
|
5: self.__class__.REQUIRED,
|
|
}
|
|
return strengths[self.strength]
|
|
|
|
|
|
# This is a terrible pattern IMO, but true to the original JS implementation.
|
|
Strength.REQUIRED = Strength(0, "required")
|
|
Strength.STONG_PREFERRED = Strength(1, "strongPreferred")
|
|
Strength.PREFERRED = Strength(2, "preferred")
|
|
Strength.STRONG_DEFAULT = Strength(3, "strongDefault")
|
|
Strength.NORMAL = Strength(4, "normal")
|
|
Strength.WEAK_DEFAULT = Strength(5, "weakDefault")
|
|
Strength.WEAKEST = Strength(6, "weakest")
|
|
|
|
|
|
class Constraint(object):
|
|
def __init__(self, strength):
|
|
super(Constraint, self).__init__()
|
|
self.strength = strength
|
|
|
|
def add_constraint(self):
|
|
global planner
|
|
self.add_to_graph()
|
|
planner.incremental_add(self)
|
|
|
|
def satisfy(self, mark):
|
|
global planner
|
|
self.choose_method(mark)
|
|
|
|
if not self.is_satisfied():
|
|
if self.strength == Strength.REQUIRED:
|
|
print('Could not satisfy a required constraint!')
|
|
|
|
return None
|
|
|
|
self.mark_inputs(mark)
|
|
out = self.output()
|
|
overridden = out.determined_by
|
|
|
|
if overridden is not None:
|
|
overridden.mark_unsatisfied()
|
|
|
|
out.determined_by = self
|
|
|
|
if not planner.add_propagate(self, mark):
|
|
print('Cycle encountered')
|
|
|
|
out.mark = mark
|
|
return overridden
|
|
|
|
def destroy_constraint(self):
|
|
global planner
|
|
if self.is_satisfied():
|
|
planner.incremental_remove(self)
|
|
else:
|
|
self.remove_from_graph()
|
|
|
|
def is_input(self):
|
|
return False
|
|
|
|
|
|
class UrnaryConstraint(Constraint):
|
|
def __init__(self, v, strength):
|
|
super(UrnaryConstraint, self).__init__(strength)
|
|
self.my_output = v
|
|
self.satisfied = False
|
|
self.add_constraint()
|
|
|
|
def add_to_graph(self):
|
|
self.my_output.add_constraint(self)
|
|
self.satisfied = False
|
|
|
|
def choose_method(self, mark):
|
|
if self.my_output.mark != mark and \
|
|
Strength.stronger(self.strength, self.my_output.walk_strength):
|
|
self.satisfied = True
|
|
else:
|
|
self.satisfied = False
|
|
|
|
def is_satisfied(self):
|
|
return self.satisfied
|
|
|
|
def mark_inputs(self, mark):
|
|
# No-ops.
|
|
pass
|
|
|
|
def output(self):
|
|
# Ugh. Keeping it for consistency with the original. So much for
|
|
# "we're all adults here"...
|
|
return self.my_output
|
|
|
|
def recalculate(self):
|
|
self.my_output.walk_strength = self.strength
|
|
self.my_output.stay = not self.is_input()
|
|
|
|
if self.my_output.stay:
|
|
self.execute()
|
|
|
|
def mark_unsatisfied(self):
|
|
self.satisfied = False
|
|
|
|
def inputs_known(self, mark):
|
|
return True
|
|
|
|
def remove_from_graph(self):
|
|
if self.my_output is not None:
|
|
self.my_output.remove_constraint(self)
|
|
self.satisfied = False
|
|
|
|
|
|
class StayConstraint(UrnaryConstraint):
|
|
def __init__(self, v, string):
|
|
super(StayConstraint, self).__init__(v, string)
|
|
|
|
def execute(self):
|
|
# The methods, THEY DO NOTHING.
|
|
pass
|
|
|
|
|
|
class EditConstraint(UrnaryConstraint):
|
|
def __init__(self, v, string):
|
|
super(EditConstraint, self).__init__(v, string)
|
|
|
|
def is_input(self):
|
|
return True
|
|
|
|
def execute(self):
|
|
# This constraint also does nothing.
|
|
pass
|
|
|
|
|
|
class Direction(object):
|
|
# Hooray for things that ought to be structs!
|
|
NONE = 0
|
|
FORWARD = 1
|
|
BACKWARD = -1
|
|
|
|
|
|
class BinaryConstraint(Constraint):
|
|
def __init__(self, v1, v2, strength):
|
|
super(BinaryConstraint, self).__init__(strength)
|
|
self.v1 = v1
|
|
self.v2 = v2
|
|
self.direction = Direction.NONE
|
|
self.add_constraint()
|
|
|
|
def choose_method(self, mark):
|
|
if self.v1.mark == mark:
|
|
if self.v2.mark != mark and Strength.stronger(self.strength, self.v2.walk_strength):
|
|
self.direction = Direction.FORWARD
|
|
else:
|
|
self.direction = Direction.BACKWARD
|
|
|
|
if self.v2.mark == mark:
|
|
if self.v1.mark != mark and Strength.stronger(self.strength, self.v1.walk_strength):
|
|
self.direction = Direction.BACKWARD
|
|
else:
|
|
self.direction = Direction.NONE
|
|
|
|
if Strength.weaker(self.v1.walk_strength, self.v2.walk_strength):
|
|
if Strength.stronger(self.strength, self.v1.walk_strength):
|
|
self.direction = Direction.BACKWARD
|
|
else:
|
|
self.direction = Direction.NONE
|
|
else:
|
|
if Strength.stronger(self.strength, self.v2.walk_strength):
|
|
self.direction = Direction.FORWARD
|
|
else:
|
|
self.direction = Direction.BACKWARD
|
|
|
|
def add_to_graph(self):
|
|
self.v1.add_constraint(self)
|
|
self.v2.add_constraint(self)
|
|
self.direction = Direction.NONE
|
|
|
|
def is_satisfied(self):
|
|
return self.direction != Direction.NONE
|
|
|
|
def mark_inputs(self, mark):
|
|
self.input().mark = mark
|
|
|
|
def input(self):
|
|
if self.direction == Direction.FORWARD:
|
|
return self.v1
|
|
|
|
return self.v2
|
|
|
|
def output(self):
|
|
if self.direction == Direction.FORWARD:
|
|
return self.v2
|
|
|
|
return self.v1
|
|
|
|
def recalculate(self):
|
|
ihn = self.input()
|
|
out = self.output()
|
|
out.walk_strength = Strength.weakest_of(self.strength, ihn.walk_strength)
|
|
out.stay = ihn.stay
|
|
|
|
if out.stay:
|
|
self.execute()
|
|
|
|
def mark_unsatisfied(self):
|
|
self.direction = Direction.NONE
|
|
|
|
def inputs_known(self, mark):
|
|
i = self.input()
|
|
return i.mark == mark or i.stay or i.determined_by == None
|
|
|
|
def remove_from_graph(self):
|
|
if self.v1 is not None:
|
|
self.v1.remove_constraint(self)
|
|
|
|
if self.v2 is not None:
|
|
self.v2.remove_constraint(self)
|
|
|
|
self.direction = Direction.NONE
|
|
|
|
|
|
class ScaleConstraint(BinaryConstraint):
|
|
def __init__(self, src, scale, offset, dest, strength):
|
|
self.direction = Direction.NONE
|
|
self.scale = scale
|
|
self.offset = offset
|
|
super(ScaleConstraint, self).__init__(src, dest, strength)
|
|
|
|
def add_to_graph(self):
|
|
super(ScaleConstraint, self).add_to_graph()
|
|
self.scale.add_constraint(self)
|
|
self.offset.add_constraint(self)
|
|
|
|
def remove_from_graph(self):
|
|
super(ScaleConstraint, self).remove_from_graph()
|
|
|
|
if self.scale is not None:
|
|
self.scale.remove_constraint(self)
|
|
|
|
if self.offset is not None:
|
|
self.offset.remove_constraint(self)
|
|
|
|
def mark_inputs(self, mark):
|
|
super(ScaleConstraint, self).mark_inputs(mark)
|
|
self.scale.mark = mark
|
|
self.offset.mark = mark
|
|
|
|
def execute(self):
|
|
if self.direction == Direction.FORWARD:
|
|
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
|
|
|
|
def recalculate(self):
|
|
ihn = self.input()
|
|
out = self.output()
|
|
out.walk_strength = Strength.weakest_of(self.strength, ihn.walk_strength)
|
|
out.stay = ihn.stay and self.scale.stay and self.offset.stay
|
|
|
|
if out.stay:
|
|
self.execute()
|
|
|
|
|
|
class EqualityConstraint(BinaryConstraint):
|
|
def execute(self):
|
|
self.output().value = self.input().value
|
|
|
|
|
|
class Variable(object):
|
|
def __init__(self, name, initial_value=0):
|
|
super(Variable, self).__init__()
|
|
self.name = name
|
|
self.value = initial_value
|
|
self.constraints = []
|
|
self.determined_by = None
|
|
self.mark = 0
|
|
self.walk_strength = Strength.WEAKEST
|
|
self.stay = True
|
|
|
|
def __repr__(self):
|
|
# To make debugging this beast from pdb easier...
|
|
return '<Variable: %s - %s>' % (
|
|
self.name,
|
|
self.value
|
|
)
|
|
|
|
def add_constraint(self, constraint):
|
|
self.constraints.append(constraint)
|
|
|
|
def remove_constraint(self, constraint):
|
|
self.constraints.remove(constraint)
|
|
|
|
if self.determined_by == constraint:
|
|
self.determined_by = None
|
|
|
|
|
|
class Planner(object):
|
|
def __init__(self):
|
|
super(Planner, self).__init__()
|
|
self.current_mark = 0
|
|
|
|
def incremental_add(self, constraint):
|
|
mark = self.new_mark()
|
|
overridden = constraint.satisfy(mark)
|
|
|
|
while overridden is not None:
|
|
overridden = overridden.satisfy(mark)
|
|
|
|
def incremental_remove(self, constraint):
|
|
out = constraint.output()
|
|
constraint.mark_unsatisfied()
|
|
constraint.remove_from_graph()
|
|
unsatisfied = self.remove_propagate_from(out)
|
|
strength = Strength.REQUIRED
|
|
# Do-while, the Python way.
|
|
repeat = True
|
|
|
|
while repeat:
|
|
for u in unsatisfied:
|
|
if u.strength == strength:
|
|
self.incremental_add(u)
|
|
|
|
strength = strength.next_weaker()
|
|
|
|
repeat = strength != Strength.WEAKEST
|
|
|
|
def new_mark(self):
|
|
self.current_mark += 1
|
|
return self.current_mark
|
|
|
|
def make_plan(self, sources):
|
|
mark = self.new_mark()
|
|
plan = Plan()
|
|
todo = sources
|
|
|
|
while len(todo):
|
|
c = todo.pop(0)
|
|
|
|
if c.output().mark != mark and c.inputs_known(mark):
|
|
plan.add_constraint(c)
|
|
c.output().mark = mark
|
|
self.add_constraints_consuming_to(c.output(), todo)
|
|
|
|
return plan
|
|
|
|
def extract_plan_from_constraints(self, constraints):
|
|
sources = []
|
|
|
|
for c in constraints:
|
|
if c.is_input() and c.is_satisfied():
|
|
sources.append(c)
|
|
|
|
return self.make_plan(sources)
|
|
|
|
def add_propagate(self, c, mark):
|
|
todo = []
|
|
todo.append(c)
|
|
|
|
while len(todo):
|
|
d = todo.pop(0)
|
|
|
|
if d.output().mark == mark:
|
|
self.incremental_remove(c)
|
|
return False
|
|
|
|
d.recalculate()
|
|
self.add_constraints_consuming_to(d.output(), todo)
|
|
|
|
return True
|
|
|
|
def remove_propagate_from(self, out):
|
|
out.determined_by = None
|
|
out.walk_strength = Strength.WEAKEST
|
|
out.stay = True
|
|
unsatisfied = []
|
|
todo = []
|
|
todo.append(out)
|
|
|
|
while len(todo):
|
|
v = todo.pop(0)
|
|
|
|
for c in v.constraints:
|
|
if not c.is_satisfied():
|
|
unsatisfied.append(c)
|
|
|
|
determining = v.determined_by
|
|
|
|
for c in v.constraints:
|
|
if c != determining and c.is_satisfied():
|
|
c.recalculate()
|
|
todo.append(c.output())
|
|
|
|
return unsatisfied
|
|
|
|
def add_constraints_consuming_to(self, 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.append(c)
|
|
|
|
|
|
class Plan(object):
|
|
def __init__(self):
|
|
super(Plan, self).__init__()
|
|
self.v = []
|
|
|
|
def add_constraint(self, c):
|
|
self.v.append(c)
|
|
|
|
def __len__(self):
|
|
return len(self.v)
|
|
|
|
def __getitem__(self, index):
|
|
return self.v[index]
|
|
|
|
def execute(self):
|
|
for c in self.v:
|
|
c.execute()
|
|
|
|
|
|
# Main
|
|
total = 0
|
|
|
|
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.
|
|
"""
|
|
global planner
|
|
global total
|
|
|
|
planner = Planner()
|
|
prev, first, last = None, None, None
|
|
|
|
# We need to go up to n inclusively.
|
|
for i in range(n + 1):
|
|
name = "v%s" % i
|
|
v = Variable(name)
|
|
|
|
if prev is not None:
|
|
EqualityConstraint(prev, v, Strength.REQUIRED)
|
|
|
|
if i == 0:
|
|
first = v
|
|
|
|
if i == n:
|
|
last = v
|
|
|
|
prev = v
|
|
|
|
StayConstraint(last, Strength.STRONG_DEFAULT)
|
|
edit = EditConstraint(first, Strength.PREFERRED)
|
|
edits = []
|
|
edits.append(edit)
|
|
plan = planner.extract_plan_from_constraints(edits)
|
|
|
|
for i in range(100):
|
|
first.value = i
|
|
plan.execute()
|
|
|
|
total += int(last.value)
|
|
if last.value != i:
|
|
print("Chain test failed.")
|
|
|
|
|
|
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.
|
|
"""
|
|
global planner
|
|
global total
|
|
|
|
planner = Planner()
|
|
scale = Variable("scale", 10)
|
|
offset = Variable("offset", 1000)
|
|
src, dest = None, None
|
|
|
|
dests = []
|
|
|
|
for i in range(n):
|
|
src = Variable("src%s" % i, i)
|
|
dst = Variable("dst%s" % i, i)
|
|
dests.append(dst)
|
|
StayConstraint(src, Strength.NORMAL)
|
|
ScaleConstraint(src, scale, offset, dst, Strength.REQUIRED)
|
|
|
|
change(src, 17)
|
|
|
|
total += int(dst.value)
|
|
if dst.value != 1170:
|
|
print("Projection 1 failed")
|
|
|
|
change(dst, 1050)
|
|
|
|
total += int(src.value)
|
|
if src.value != 5:
|
|
print("Projection 2 failed")
|
|
|
|
change(scale, 5)
|
|
|
|
for i in range(n - 1):
|
|
total += int(dests[i].value)
|
|
if dests[i].value != (i * 5 + 1000):
|
|
print("Projection 3 failed")
|
|
|
|
change(offset, 2000)
|
|
|
|
for i in range(n - 1):
|
|
total += int(dests[i].value)
|
|
if dests[i].value != (i * 5 + 2000):
|
|
print("Projection 4 failed")
|
|
|
|
|
|
def change(v, new_value):
|
|
global planner
|
|
edit = EditConstraint(v, Strength.PREFERRED)
|
|
edits = []
|
|
edits.append(edit)
|
|
|
|
plan = planner.extract_plan_from_constraints(edits)
|
|
|
|
for i in range(10):
|
|
v.value = new_value
|
|
plan.execute()
|
|
|
|
edit.destroy_constraint()
|
|
|
|
|
|
# HOORAY FOR GLOBALS... Oh wait.
|
|
# In spirit of the original, we'll keep it, but ugh.
|
|
planner = None
|
|
|
|
|
|
def delta_blue():
|
|
global total
|
|
start = time.clock()
|
|
for i in range(20):
|
|
chain_test(100)
|
|
projection_test(100)
|
|
print(total)
|
|
print("elapsed: " + str(time.clock() - start))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
delta_blue() |