From 531ddce6a08630985160b1f5d29cad1ab25ebdd5 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Wed, 27 Jan 2021 22:24:28 +0200 Subject: [PATCH] Added dependency tracking to computations for future optimization effort --- .../api/creature/CreatureProperties.js | 10 ++++++++++ .../creature/computation/ComputationMemo.js | 3 +++ .../creature/computation/EffectAggregator.js | 7 ++++++- .../api/creature/computation/applyToggles.js | 1 + .../api/creature/computation/combineStat.js | 13 +++++++++++- .../api/creature/computation/computeEffect.js | 7 ++++++- .../computation/computeEndStepProperty.js | 16 +++++++++++++-- .../api/creature/computation/computeLevels.js | 10 ++++++++-- .../api/creature/computation/computeStat.js | 2 ++ .../api/creature/computation/computeToggle.js | 7 ++++++- .../computation/evaluateCalculation.js | 16 +++++++++------ .../creature/computation/recomputeCreature.js | 20 +++++++++++++++++++ .../computation/writeAlteredProperties.js | 3 ++- 13 files changed, 100 insertions(+), 15 deletions(-) diff --git a/app/imports/api/creature/CreatureProperties.js b/app/imports/api/creature/CreatureProperties.js index 66c8f896..89ed39bc 100644 --- a/app/imports/api/creature/CreatureProperties.js +++ b/app/imports/api/creature/CreatureProperties.js @@ -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){ diff --git a/app/imports/api/creature/computation/ComputationMemo.js b/app/imports/api/creature/computation/ComputationMemo.js index ffb2a7ab..94a3e90f 100644 --- a/app/imports/api/creature/computation/ComputationMemo.js +++ b/app/imports/api/creature/computation/ComputationMemo.js @@ -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 { diff --git a/app/imports/api/creature/computation/EffectAggregator.js b/app/imports/api/creature/computation/EffectAggregator.js index bb127e6d..4419ffd0 100644 --- a/app/imports/api/creature/computation/EffectAggregator.js +++ b/app/imports/api/creature/computation/EffectAggregator.js @@ -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; } diff --git a/app/imports/api/creature/computation/applyToggles.js b/app/imports/api/creature/computation/applyToggles.js index cdcda3fd..16720f51 100644 --- a/app/imports/api/creature/computation/applyToggles.js +++ b/app/imports/api/creature/computation/applyToggles.js @@ -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; } diff --git a/app/imports/api/creature/computation/combineStat.js b/app/imports/api/creature/computation/combineStat.js index efef3df9..dc3dd13d 100644 --- a/app/imports/api/creature/computation/combineStat.js +++ b/app/imports/api/creature/computation/combineStat.js @@ -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; diff --git a/app/imports/api/creature/computation/computeEffect.js b/app/imports/api/creature/computation/computeEffect.js index b4507bc6..76ae68e2 100644 --- a/app/imports/api/creature/computation/computeEffect.js +++ b/app/imports/api/creature/computation/computeEffect.js @@ -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; } diff --git a/app/imports/api/creature/computation/computeEndStepProperty.js b/app/imports/api/creature/computation/computeEndStepProperty.js index dbf7113d..07494e15 100644 --- a/app/imports/api/creature/computation/computeEndStepProperty.js +++ b/app/imports/api/creature/computation/computeEndStepProperty.js @@ -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 { diff --git a/app/imports/api/creature/computation/computeLevels.js b/app/imports/api/creature/computation/computeLevels.js index 81e1dd4d..41aad1f2 100644 --- a/app/imports/api/creature/computation/computeLevels.js +++ b/app/imports/api/creature/computation/computeLevels.js @@ -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; } diff --git a/app/imports/api/creature/computation/computeStat.js b/app/imports/api/creature/computation/computeStat.js index 291fb5dd..dc910264 100644 --- a/app/imports/api/creature/computation/computeStat.js +++ b/app/imports/api/creature/computation/computeStat.js @@ -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); } diff --git a/app/imports/api/creature/computation/computeToggle.js b/app/imports/api/creature/computation/computeToggle.js index 04b8b801..2783e2b2 100644 --- a/app/imports/api/creature/computation/computeToggle.js +++ b/app/imports/api/creature/computation/computeToggle.js @@ -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; } diff --git a/app/imports/api/creature/computation/evaluateCalculation.js b/app/imports/api/creature/computation/evaluateCalculation.js index 02bff69f..bc8fa5a5 100644 --- a/app/imports/api/creature/computation/evaluateCalculation.js +++ b/app/imports/api/creature/computation/evaluateCalculation.js @@ -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}; } diff --git a/app/imports/api/creature/computation/recomputeCreature.js b/app/imports/api/creature/computation/recomputeCreature.js index 6e4babd6..6a9397f3 100644 --- a/app/imports/api/creature/computation/recomputeCreature.js +++ b/app/imports/api/creature/computation/recomputeCreature.js @@ -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; } diff --git a/app/imports/api/creature/computation/writeAlteredProperties.js b/app/imports/api/creature/computation/writeAlteredProperties.js index 9c34d071..bf2c505f 100644 --- a/app/imports/api/creature/computation/writeAlteredProperties.js +++ b/app/imports/api/creature/computation/writeAlteredProperties.js @@ -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); }