Parsed calculations are now cached between calculations
Parsing is one of the more expensive computations done to characters, so the parser results are now stored on the DB and only updated if they are dirty. A hash is used to determine if the calculation has changed since the last computation
This commit is contained in:
@@ -8,7 +8,7 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop
|
||||
// ancestors: {} //this gets added if there are resolved ancestors
|
||||
};
|
||||
// Traverse the parsed calculation looking for variable names
|
||||
traverse(calcObj._parsedCalculation, node => {
|
||||
traverse(calcObj.parseNode, node => {
|
||||
// Skip nodes that aren't symbols or accessors
|
||||
if (node.parseType !== 'symbol' && node.parseType !== 'accessor') return;
|
||||
// Link ancestor references as direct property dependencies
|
||||
|
||||
@@ -2,7 +2,8 @@ import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX
|
||||
import { prettifyParseError, parse } from '/imports/parser/parser.js';
|
||||
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
|
||||
import { get } from 'lodash';
|
||||
import errorNode from '/imports/parser/parseTree/error.js'
|
||||
import errorNode from '/imports/parser/parseTree/error.js';
|
||||
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
|
||||
|
||||
export default function parseCalculationFields(prop, schemas){
|
||||
discoverInlineCalculationFields(prop, schemas);
|
||||
@@ -20,7 +21,15 @@ function discoverInlineCalculationFields(prop, schemas){
|
||||
prop._computationDetails.inlineCalculations.push(inlineCalcObj);
|
||||
// Extract the calculations and store them on the property
|
||||
let string = inlineCalcObj.text;
|
||||
if (!string) return;
|
||||
if (!string){
|
||||
delete inlineCalcObj.hash;
|
||||
return;
|
||||
}
|
||||
const inlineCalcHash = cyrb53(inlineCalcObj.text);
|
||||
if (inlineCalcHash === inlineCalcObj.hash){
|
||||
return;
|
||||
}
|
||||
inlineCalcObj.hash = inlineCalcHash;
|
||||
inlineCalcObj.inlineCalculations = [];
|
||||
let matches = string.matchAll(INLINE_CALCULATION_REGEX);
|
||||
for (let match of matches){
|
||||
@@ -55,17 +64,27 @@ function parseAllCalculationFields(prop, schemas){
|
||||
}
|
||||
|
||||
function parseCalculation(calcObj){
|
||||
if (!calcObj.calculation) return;
|
||||
// If there is no calculation clear the cached parse node and error
|
||||
if (!calcObj.calculation){
|
||||
delete calcObj.hash;
|
||||
delete calcObj.parseError;
|
||||
return;
|
||||
}
|
||||
const calcHash = cyrb53(calcObj.calculation);
|
||||
// If the cached parse calculation is equal to the calculation, skip
|
||||
if (calcHash === calcObj.hash){
|
||||
return;
|
||||
}
|
||||
calcObj.hash = calcHash;
|
||||
try {
|
||||
calcObj._parsedCalculation = parse(calcObj.calculation);
|
||||
calcObj.parseNode = parse(calcObj.calculation);
|
||||
delete calcObj.parseError;
|
||||
} catch (e) {
|
||||
let error = {
|
||||
type: 'evaluation',
|
||||
message: prettifyParseError(e),
|
||||
};
|
||||
calcObj.errors ?
|
||||
calcObj.errors.push(error) :
|
||||
calcObj.errors = [error];
|
||||
calcObj._parsedCalculation = errorNode.create({error});
|
||||
calcObj.parseError = error;
|
||||
calcObj.parseNode = errorNode.create({error});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ export default function(){
|
||||
'Variable references create dependencies even if the attributes don\'t exist'
|
||||
);
|
||||
assert.equal(
|
||||
prop('strengthId').baseValue.errors.length, 1,
|
||||
'Parse errors should be added to calculation errors'
|
||||
prop('strengthId').baseValue.parseError.message, 'Unexpected end of input',
|
||||
'Parse errors should be stored on the calculation doc'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,5 @@ export function buildComputationFromProps(properties){
|
||||
linkTypeDependencies(dependencyGraph, prop, computation);
|
||||
linkCalculationDependencies(dependencyGraph, prop, computation);
|
||||
});
|
||||
|
||||
return computation;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ function computeResources(computation, node){
|
||||
resources.attributesConsumed.forEach(attConsumed => {
|
||||
if (!attConsumed.variableName) return;
|
||||
const att = computation.scope[attConsumed.variableName];
|
||||
if (!att._id) return;
|
||||
attConsumed.available = att.value;
|
||||
attConsumed.statId = att._id;
|
||||
attConsumed.statName = att.name;
|
||||
|
||||
@@ -13,25 +13,20 @@ export default function computeCalculations(computation, node){
|
||||
}
|
||||
|
||||
function evaluateCalculation(calculation, scope){
|
||||
const parseNode = calculation._parsedCalculation;
|
||||
const parseNode = calculation.parseNode;
|
||||
const fn = calculation._parseLevel;
|
||||
const calculationScope = {...calculation._localScope, ...scope};
|
||||
const {result: resultNode, context} = resolve(fn, parseNode, calculationScope);
|
||||
if (resultNode.parseType === 'constant'){
|
||||
calculation.errors = context.errors;
|
||||
if (resultNode?.parseType === 'constant'){
|
||||
calculation.value = resultNode.value;
|
||||
} else if (resultNode.parseType === 'error'){
|
||||
} else if (resultNode?.parseType === 'error'){
|
||||
calculation.value = null;
|
||||
} else {
|
||||
calculation.value = toString(resultNode);
|
||||
}
|
||||
if (calculation.errors){
|
||||
calculation.errors = [...calculation.errors, ...context.errors]
|
||||
} else {
|
||||
calculation.errors = context.errors
|
||||
}
|
||||
// remove the working fields
|
||||
delete calculation._parseLevel;
|
||||
delete calculation._parsedCalculation;
|
||||
delete calculation._localScope;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,6 @@ export default function(){
|
||||
assert.equal(scope('strength').modifier, 1);
|
||||
assert.equal(prop('referencesDexId').value, 4);
|
||||
assert.equal(prop('hitDiceId').constitutionMod, 5);
|
||||
assert.equal(
|
||||
prop('parseErrorId').baseValue.errors.length, 1,
|
||||
'Parse errors should be added to calculation errors'
|
||||
);
|
||||
assert.equal(
|
||||
prop('parseErrorId').baseValue.value, null,
|
||||
'Parse errors should null the value'
|
||||
|
||||
14
app/imports/api/engine/computation/utility/cyrb53.js
Normal file
14
app/imports/api/engine/computation/utility/cyrb53.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// Simple hash function from
|
||||
// https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
|
||||
// Don't use for security
|
||||
export default function(str, seed = 0) {
|
||||
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);
|
||||
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
|
||||
return 4294967296 * (2097151 & h2) + (h1>>>0);
|
||||
}
|
||||
@@ -24,6 +24,22 @@ function computedOnlyField(field){
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
// A cache of the parse result of the calculation
|
||||
[`${field}.parseNode`]: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
blackbox: true,
|
||||
},
|
||||
// Set if there was an error parsing the calculation
|
||||
[`${field}.parseError`]: {
|
||||
type: ErrorSchema,
|
||||
optional: true,
|
||||
},
|
||||
// a hash of the calculation to see if the cached values need to be updated
|
||||
[`${field}.hash`]: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
[`${field}.errors`]: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
|
||||
@@ -28,6 +28,12 @@ function computedOnlyInlineCalculationField(field){
|
||||
optional: true,
|
||||
inlineCalculationField: true,
|
||||
},
|
||||
// a hash of the text to see if the current cached values need to be updated
|
||||
[`${field}.hash`]: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.inlineCalculationField,
|
||||
},
|
||||
[`${field}.value`]: {
|
||||
type: String,
|
||||
optional: true,
|
||||
@@ -38,7 +44,6 @@ function computedOnlyInlineCalculationField(field){
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: STORAGE_LIMITS.inlineCalculationCount,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
[`${field}.inlineCalculations.$`]: {
|
||||
type: Object,
|
||||
@@ -50,15 +55,34 @@ function computedOnlyInlineCalculationField(field){
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.calculation,
|
||||
},
|
||||
// The result of the calc
|
||||
[`${field}.inlineCalculations.$.value`]: {
|
||||
type: SimpleSchema.oneOf(String, Number),
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.calculation,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
// A cache of the parse result of the calculation
|
||||
[`${field}.inlineCalculations.$.parseNode`]: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
blackbox: true,
|
||||
},
|
||||
// Set if there was an error parsing the calculation
|
||||
[`${field}.inlineCalculations.$.parseError`]: {
|
||||
type: ErrorSchema,
|
||||
optional: true,
|
||||
},
|
||||
// a hash of the calculation to see if the cached values need to be updated
|
||||
[`${field}.inlineCalculations.$.hash`]: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
[`${field}.inlineCalculations.$.errors`]: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
maxCount: STORAGE_LIMITS.errorCount,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
[`${field}.inlineCalculations.$.errors.$`]: {
|
||||
type: ErrorSchema,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import resolve, { traverse, toString } from '../resolve';
|
||||
import error from './error';
|
||||
|
||||
const index = {
|
||||
const indexNode = {
|
||||
create({array, index}) {
|
||||
return {
|
||||
parseType: 'index',
|
||||
@@ -53,7 +53,7 @@ const index = {
|
||||
}
|
||||
}
|
||||
return {
|
||||
result: index.create({
|
||||
result: indexNode.create({
|
||||
index,
|
||||
array,
|
||||
}),
|
||||
@@ -70,4 +70,4 @@ const index = {
|
||||
},
|
||||
}
|
||||
|
||||
export default index;
|
||||
export default indexNode;
|
||||
|
||||
@@ -3,6 +3,7 @@ import nodeTypeIndex from './parseTree/_index.js';
|
||||
// Takes a parse ndoe and computes it to a set detail level
|
||||
// returns {result, context}
|
||||
export default function resolve(fn, node, scope, context = new Context()){
|
||||
if (!node) return {result: undefined, context};
|
||||
let type = nodeTypeIndex[node.parseType];
|
||||
if (!type){
|
||||
throw new Meteor.Error(`Parse node type: ${node.parseType} not implemented`);
|
||||
@@ -21,6 +22,7 @@ export default function resolve(fn, node, scope, context = new Context()){
|
||||
}
|
||||
|
||||
export function toString(node){
|
||||
if (!node) return '';
|
||||
let type = nodeTypeIndex[node.parseType];
|
||||
if (!type.toString){
|
||||
throw new Meteor.Error('toString not implemented on ' + node.parseType);
|
||||
|
||||
Reference in New Issue
Block a user