Added dependency tracking to computations for future optimization effort
This commit is contained in:
@@ -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){
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user