Added dependency tracking to computations for future optimization effort

This commit is contained in:
Stefan Zermatten
2021-01-27 22:24:28 +02:00
parent a4e6dd1d66
commit 531ddce6a0
13 changed files with 100 additions and 15 deletions

View File

@@ -62,6 +62,16 @@ let CreaturePropertySchema = new SimpleSchema({
optional: true,
index: 1,
},
// Denormalised list of all properties or creatures this property depends on
dependencies: {
type: Array,
defaultValue: [],
index: 1,
},
'dependencies.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
});
for (let key in propertySchemasIndex){

View File

@@ -74,6 +74,7 @@ export default class ComputationMemo {
registerProperty(prop){
this.originalPropsById[prop._id] = cloneDeep(prop);
this.propsById[prop._id] = prop;
prop.dependencies = [];
prop.computationDetails = propDetails(prop);
prop.ancestors.forEach(ancestor => {
if (this.toggleIds.has(ancestor.id)){
@@ -104,6 +105,7 @@ export default class ComputationMemo {
stats: [variableName],
computationDetails: propDetailsByType.effect(),
statBase: true,
dependencies: [],
});
}
if (prop.baseProficiency){
@@ -113,6 +115,7 @@ export default class ComputationMemo {
computationDetails: propDetailsByType.proficiency(),
type: 'proficiency',
statBase: true,
dependencies: [],
});
}
} else {

View File

@@ -4,8 +4,13 @@ export default class EffectAggregator{
constructor(stat, memo){
delete this.baseValueErrors;
if (stat.baseValueCalculation){
let {result, context} = evaluateCalculation(stat.baseValueCalculation, memo);
let {
result,
context,
dependencies
} = evaluateCalculation(stat.baseValueCalculation, memo);
this.statBaseValue = result.value;
stat.dependencies.push(...dependencies);
if (context.errors.length){
this.baseValueErrors = context.errors;
}

View File

@@ -4,6 +4,7 @@ export default function applyToggles(prop, memo){
prop.computationDetails.toggleAncestors.forEach(toggleId => {
let toggle = memo.togglesById[toggleId];
computeToggle(toggle, memo);
prop.dependencies.push(toggle._id, ...toggle.dependencies);
if (!toggle.toggleResult){
prop.computationDetails.disabledByToggle = true;
}

View File

@@ -34,9 +34,14 @@ function combineAttribute(stat, aggregator, memo){
stat.baseValue = aggregator.statBaseValue;
stat.baseValueErrors = aggregator.baseValueErrors;
if (stat.attributeType === 'spellSlot'){
let {result, context} = evaluateCalculation(stat.spellSlotLevelCalculation, memo);
let {
result,
context,
dependencies
} = evaluateCalculation(stat.spellSlotLevelCalculation, memo);
stat.spellSlotLevelValue = result.value;
stat.spellSlotLevelErrors = context.errors;
stat.dependencies.push(...dependencies);
}
stat.currentValue = stat.value - (stat.damage || 0);
if (stat.attributeType === 'ability') {
@@ -55,6 +60,7 @@ function combineSkill(stat, aggregator, memo){
computeStat(ability, memo);
}
stat.abilityMod = ability.modifier;
stat.dependencies.push(ability._id, ...ability.dependencies);
}
// Combine all the child proficiencies
stat.proficiency = stat.baseProficiency || 0;
@@ -66,6 +72,7 @@ function combineSkill(stat, aggregator, memo){
prof.value > stat.proficiency
){
stat.proficiency = prof.value;
stat.dependencies.push(prof._id, ...prof.dependencies);
}
}
// Get the character's proficiency bonus to apply
@@ -75,6 +82,10 @@ function combineSkill(stat, aggregator, memo){
if (typeof profBonus !== 'number' && memo.statsByVariableName['level']){
let level = memo.statsByVariableName['level'].value;
profBonus = Math.ceil(level / 4) + 1;
if (level._id) stat.dependencies.push(level._id);
if (level.dependencies) stat.dependencies.push(...level.dependencies);
} else {
stat.dependencies.push(profBonusStat._id, ...profBonusStat.dependencies);
}
// Multiply the proficiency bonus by the actual proficiency
profBonus *= stat.proficiency;

View File

@@ -34,8 +34,13 @@ export default function computeEffect(effect, memo){
} else if(_.contains(['advantage', 'disadvantage', 'fail'], effect.operation)){
effect.result = 1;
} else {
let {result, context} = evaluateCalculation(effect.calculation, memo);
let {
result,
context,
dependencies,
} = evaluateCalculation(effect.calculation, memo);
effect.result = result.value;
effect.dependencies.push(...dependencies);
if (context.errors.length){
effect.errors = context.errors;
}

View File

@@ -23,8 +23,13 @@ export default function computeEndStepProperty(prop, memo){
function computeAction(prop, memo){
// Uses
let {result, context} = evaluateCalculation(prop.uses, memo);
let {
result,
context,
dependencies,
} = evaluateCalculation(prop.uses, memo);
prop.usesResult = result.value;
prop.dependencies.push(...dependencies);
if (context.errors.length){
prop.usesErrors = context.errors;
} else {
@@ -46,6 +51,7 @@ function computeAction(prop, memo){
if (available < attConsumed.quantity){
prop.insufficientResources = true;
}
if (stat) prop.dependencies.push(stat._id, ...stat.dependencies);
}
});
// Items consumed
@@ -64,12 +70,18 @@ function computeAction(prop, memo){
if (!item || available < itemConsumed.quantity){
prop.insufficientResources = true;
}
if (item) prop.dependencies.push(item._id, ...item.dependencies);
});
}
function computePropertyField(prop, memo, fieldName, fn){
let {result, context} = evaluateCalculation(prop[fieldName], memo, fn);
let {
result,
context,
dependencies,
} = evaluateCalculation(prop[fieldName], memo, fn);
prop[`${fieldName}Result`] = result.value;
prop.dependencies.push(...dependencies);
if (context.errors.length){
prop[`${fieldName}Errors`] = context.errors;
} else {

View File

@@ -7,6 +7,8 @@ export default function computeLevels(memo){
function computeClassLevels(memo){
forOwn(memo.classLevelsById, classLevel => {
// class levels are mutually dependent
classLevel.dependencies.push(Object.keys(memo.classLevelsById));
let name = classLevel.variableName;
let stat = memo.statsByVariableName[name];
if (!stat){
@@ -27,6 +29,7 @@ function computeTotalLevel(memo){
if (!currentLevel){
currentLevel = {
value: 0,
dependencies: [],
computationDetails: {
builtIn: true,
computed: true,
@@ -38,7 +41,10 @@ function computeTotalLevel(memo){
if (!currentLevel.computationDetails.builtIn) return;
let level = 0;
for (let name in memo.classes){
level += memo.classes[name].level || 0;
let cls = memo.classes[name];
level += cls.level || 0;
if (cls._id) currentLevel.dependencies.push(cls._id);
if (cls.dependencies) currentLevel.dependencies.push(...cls.dependencies);
}
memo.statsByVariableName['level'].value = level;
currentLevel.value = level;
}

View File

@@ -27,6 +27,8 @@ export default function computeStat(stat, memo){
let aggregator = new EffectAggregator(stat, memo)
each(stat.computationDetails.effects, (effect) => {
computeEffect(effect, memo);
if (effect._id) stat.dependencies.push(effect._id);
stat.dependencies.push(...effect.dependencies);
if (!effect.computationDetails.disabledByToggle){
aggregator.addEffect(effect);
}

View File

@@ -26,8 +26,13 @@ export default function computeToggle(toggle, memo){
} else if (Number.isFinite(+toggle.condition)){
toggle.toggleResult = !!+toggle.condition;
} else {
let {result, context} = evaluateCalculation(toggle.condition, memo);
let {
result,
context,
dependencies,
} = evaluateCalculation(toggle.condition, memo);
toggle.toggleResult = !!result.value;
toggle.dependencies.push(...dependencies);
if (context.errors.length){
toggle.errors = context.errors;
}

View File

@@ -6,11 +6,13 @@ import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
/* Convert a calculation into a constant output and errors*/
export default function evaluateCalculation(string, memo, fn = 'reduce'){
if (!string) return {
context: {errors: []},
result: new ConstantNode({value: string, type: 'string'}),
};
let dependencies = [];
let errors = [];
if (!string) return {
context: {errors},
result: new ConstantNode({value: string, type: 'string'}),
dependencies,
};
// Parse the string
let calc;
try {
@@ -23,19 +25,21 @@ export default function evaluateCalculation(string, memo, fn = 'reduce'){
return {
context: {errors},
result: new ConstantNode({value: string, type: 'string'}),
dependencies,
};
}
// Ensure all symbol nodes are defined and coputed
// Ensure all symbol nodes are defined and computed
calc.traverse(node => {
if (node instanceof SymbolNode || node instanceof AccessorNode){
let stat = memo.statsByVariableName[node.name];
if (stat && !stat.computationDetails.computed){
computeStat(stat, memo);
}
if (stat) dependencies.push(stat._id || node.name, ...stat.dependencies);
}
});
// Evaluate
let context = new CompilationContext();
let result = calc[fn](memo.statsByVariableName, context);
return {result, context};
return {result, context, dependencies};
}

View File

@@ -96,7 +96,9 @@ export function recomputeCreatureById(creatureId){
* - Write the computed results back to the database
*/
export function recomputeCreatureByDoc(creature){
console.time('recomputeCreatureByDoc');
const creatureId = creature._id;
console.time('findToggles');
// find all toggles that have conditions, even if they are inactive
let toggleIds = CreatureProperties.find({
'ancestors.id': creatureId,
@@ -106,6 +108,8 @@ export function recomputeCreatureByDoc(creature){
}, {
fields: {_id: 1},
}).map(t => t._id);
console.timeEnd('findToggles');
console.time('findActiveProperties');
// Find all the active properties
let props = CreatureProperties.find({
'ancestors.id': creatureId,
@@ -127,12 +131,28 @@ export function recomputeCreatureByDoc(creature){
order: 1,
}
}).fetch();
console.timeEnd('findActiveProperties');
console.time('build computation memo');
let computationMemo = new ComputationMemo(props, creature);
console.timeEnd('build computation memo');
console.time('recomputeInactiveProperties');
recomputeInactiveProperties(creatureId);
console.timeEnd('recomputeInactiveProperties');
console.time('computeMemo');
computeMemo(computationMemo);
console.timeEnd('computeMemo');
console.time('writeAlteredProperties');
writeAlteredProperties(computationMemo);
console.timeEnd('writeAlteredProperties');
console.time('writeCreatureVariables');
writeCreatureVariables(computationMemo, creatureId);
console.timeEnd('writeCreatureVariables');
console.time('recomputeDamageMultipliersById');
recomputeDamageMultipliersById(creatureId);
console.timeEnd('recomputeDamageMultipliersById');
console.time('recomputeSlotFullness');
recomputeSlotFullness(creatureId);
console.timeEnd('recomputeSlotFullness');
console.timeEnd('recomputeCreatureByDoc');
return computationMemo;
}

View File

@@ -44,7 +44,8 @@ export default function writeAlteredProperties(memo){
ids.forEach(id => {
let op = undefined;
let original = memo.originalPropsById[id];
op = addChangedKeysToOp(op, schema.objectKeys(), original, changed);
let keys = ['dependencies', ...schema.objectKeys()];
op = addChangedKeysToOp(op, keys, original, changed);
if (op){
bulkWriteOperations.push(op);
}