How to make rules more efficient

1. Modified fields contain fields that have no influence on rule execution result.

Example:

Rule reacts to Feature creation and modification.
Modified fields filters contains fields: Release, Effort.
Rule syncs release of child User Stories
Rule action:

const api = context.getService("targetprocess/api/v2");
const featureId = args.ResourceId;
const releaseId = args.Current.Release ? args.Current.Release.Id : null;

const measurements = await api.queryAsync("Userstory", {
  select: "id",
  where: `feature.id = ${featureId}`
});

return measurements.map(id => {
  return {
    command: "targetprocess:UpdateResource",
    payload: {
      resourceType: "Userstory",
      resourceId: id,
      fields: {
        Release: releaseId === null ? null : { id: releaseId }
      }
    }
  }
});

Solution:

Rule execution does not depend on Effort. So these fields can be excluded from modified fields filter to avoid not needed rule trigerrings.

2. Loops with http api requests that don't depend on loop iteration.

Example:

const id = 1;
...
for (let i = 0; i++; i < countOfIterations) {
    ...
    // This api request does not depend on iteration of the loop
    const api = require("targetprocess/api/v2");
    const response = await api.queryAsync("feature", {
        select: "name",
        where: `feature.id = ${id}`
    });
    ...
}

Solution:

Move independent request outside the loop:

const id = 1;
const api = require("targetprocess/api/v2");
const response = await api.queryAsync("userstory", {
    select: "name",
    where: `feature.id = ${id}`
});
...
for (let i = 0; i++; i < countOfIterations) {
    ...
}

3. Several http api requests, that can be merged into one request.

Example:

const api = require("targetprocess/api/v2");
const featureIds = [1, 2, 3, 4, 5];
...
for (let featureId of featureIds) {
    ...
    const userStoriesForFeature = await api.queryAsync("userstory", {
        select: "name",
        where: `feature.id = ${featureId}`
    });
    ...
}

It doesn't really makes sense to send separate requests for user stories here. They all can be merged into single one and filtered by feature.id later if necessary.

Solution:

const api = require("targetprocess/api/v2");
const featureIds = [1, 2, 3, 4, 5];
const allUsertories = await api.queryAsync("userstory", {
    select: "name,feature:{feature.id}",
    where: `feature.id in [${featureIds.join(',')}]`
});
...
for (let featureId of featureIds) {
    ...
    userStoriesForFeature = allUsertories.filter(us => us.feature.id = featureId);
    ...
}

4. Sequential http independent api requests, that can executed in parallel.

Example 1:

const api = require("targetprocess/api/v2");
const projects = await api.queryAsync("project", {
    select: "name",
    where: `id = 1`
});

const features = await api.queryAsync("feature", {
    select: "name",
    where: `effort > 0`
});

const stories = await api.queryAsync("userstory", {
    select: "name",
    where: `tasks.count > 0`
});

Requests for projects, features and stories do not depend on each other here.
We don't need to wait for one request to finish to start next one.

Solution:

const api = require("targetprocess/api/v2");

const [projects, features, stories] = await Promise.all([
    api.queryAsync("project", {
        select: "name",
        where: `id = 1`
    }),
    api.queryAsync("feature", {
        select: "name",
        where: `effort > 0`
    }),
    api.queryAsync("userstory", {
        select: "name",
        where: `tasks.count > 0`
    })]);

Example 2. Sequential http independent api requests, that depend on loop iteration:

const api = require("targetprocess/api/v2");
const entityTypesToRead = ['project', 'feature', 'userstory'];

for (let entityType of entityTypesToRead) {
    //Part one work in loop
    ...
    const someComlexIterationDependentFilter = ...;
    const response = await api.queryAsync(entityType, {
        select: "name",
        where: someComlexIterationDependentFilter
    });
    //Part two work in loop
    ...
}

Requests in loop do not depend on each others results. We can execute them in parallel manner.

Solution:

const api = require("targetprocess/api/v2");
const entityTypesToRead = ['project', 'feature', 'userstory'];

await Promise.all(entityTypesToRead.map(async entityType => {
    //Part one work in loop
    ...
    const someComlexIterationDependentFilter = ...;
    const response = await api.queryAsync(entityType, {
        select: "name",
        where: someComlexIterationDependentFilter
    });
     //Part two work in loop
     ...
}));

5. Reading too much not needed data

Here we have JavaScript action of a rule reacts to update of UserStory. And we need to read some custom field of parent project.

Example

...
const entityId = args.ResourceId;
const entityType = args.Current.EntityType.Name;
const projectId = args.Current.Project.Id;
const api = context.getService("targetprocess/api/v2");
const projects = await api.queryAsync("UserStory", {
    select: "project.ProjectCF",
    where: `project.id=${projectId}`
});
const projectCf = projects[0];
...

There is no need to read all user stories of the parent project here. We can read project directly.

Solution:

...
const entityId = args.ResourceId;
const entityName = args.Current.Name;
const entityType = args.Current.EntityType.Name;
const projectId = args.Current.Project.Id;
const api = context.getService("targetprocess/api/v2");
const projects = await api.queryAsync("Project", {
    select: "ProjectCF",
    where: `id=${projectId}`
});
const projectCf = projects[0];
...

6. CPU Heavy operations in nested loops

Example 1

We have rule that syncronizes planned start/end dates across some hierarchy.
It uses function getDateTime that parses date from string via reqexp inside loop

function getDateTime(d) {
    return new Date(/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d)([+-][0-2]\d:[0-5]\d|Z)/.exec(d)[1]);
};
...
for (let i = 0; i < countOfIterations; i++) {
    ...
    for (let j = 0; j < countOfIterations2; j++) {
        ...
        const someDate = getDateTime(someDateStr);
        ...
    }
    ...
}

Using regular expression for parsing is really expensive in terms of CPU usage. Using it in a loop or nested loop may lead to extreme CPU consumption by rule. If there is a heuristic that date strings in similar example have good chance to repeat at different iterations of loop, a good solution for reducing CPU consumption may be memorization.

Solution:

const getDateTimeMemo = new Map();
const regexp = new RegExp(/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d)([+-][0-2]\d:[0-5]\d|Z)/);
function getDateTimeMemoized(d) {
    let date = getDateTimeMemo.get(d);
    if (!date) {
        date = new Date(regexp.exec(d)[1]);
        getDateTimeMemo.set(d, date);
    }
    return date;
};
...
for (let i = 0; i < countOfIterations; i++) {
    ...
    for (let j = 0; j < countOfIterations2; j++) {
        ...
        const someDate = getDateTimeMemoized(someDateStr);
        ...
    }
    ...
}

7. Updating nested non ExD collections with big number of elements

Example 1

In case if collection of the parent entity already contains big number of items, updating the nested collection could be inneficient and costly. This is relevant for the collections, which don't belong to the extendable domain. In this example a list of tasks is used to update Tasks collection of the specified UserStory

const utils = require("utils");
const cmds = [];
const parentUserStoryId = 99;

function tasksProcessor(tasks) {
	const items = [];
  tasks.forEach(item => {
    items.push(createTask(item));
  })
  cmds.push(utils.updateResource('UserStory', parentUserStoryId, { Tasks: { Items: items }}));
}
...
function createTask(taskDefinition) {
  return {
    Name: taskDefinition.Name,
    Effort: 0
  }
}
...

Solution:

Instead of updating nested collections, it's possible to create entities with specified parent entity. Such approach is more efficient from the performance perspective.

const utils = require("utils");
const cmds = [];
const parentUserStoryId = 99;

function tasksProcessor(tasks) {
  tasks.forEach(item => {
    items.push();
    cmds.push(utils.createResource('Task', createTask(item));
  })
}
...
function createTask(taskDefinition) {
  return {
    Name: taskDefinition.Name,
    UserStory: { Id: parentUserStoryId },
    Effort: 0
  }
}
...