1
0
Fork 0
arangodb/js/server/modules/@arangodb/aql-profiler-test-helper.js

791 lines
26 KiB
JavaScript

/*jshint globalstrict:true, strict:true, esnext: true */
"use strict";
////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2018 ArangoDB GmbH, Cologne, Germany
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is ArangoDB GmbH, Cologne, Germany
///
/// @author Tobias Gödderz
////////////////////////////////////////////////////////////////////////////////
const _ = require('lodash');
const db = require('@arangodb').db;
const expect = require('chai').expect;
const jsunity = require("jsunity");
const assert = jsunity.jsUnity.assertions;
////////////////////////////////////////////////////////////////////////////////
/// @file common variables and functions for aql-profiler* tests
////////////////////////////////////////////////////////////////////////////////
const colName = 'UnitTestProfilerCol';
const edgeColName = 'UnitTestProfilerEdgeCol';
const viewName = 'UnitTestProfilerView';
const defaultBatchSize = 1000;
const defaultTestRowCounts = [1, 2, 10, 100, 999, 1000, 1001, 1500, 2000, 10500];
const defaultClusterTestRowCounts = [1, 999, 1000, 1001];
const CalculationNode = 'CalculationNode';
const CollectNode = 'CollectNode';
const DistributeNode = 'DistributeNode';
const EnumerateCollectionNode = 'EnumerateCollectionNode';
const EnumerateListNode = 'EnumerateListNode';
const EnumerateViewNode = 'EnumerateViewNode';
const FilterNode = 'FilterNode';
const GatherNode = 'GatherNode';
const IndexNode = 'IndexNode';
const InsertNode = 'InsertNode';
const LimitNode = 'LimitNode';
const NoResultsNode = 'NoResultsNode';
const RemoteNode = 'RemoteNode';
const RemoveNode = 'RemoveNode';
const ReplaceNode = 'ReplaceNode';
const ReturnNode = 'ReturnNode';
const ScatterNode = 'ScatterNode';
const ShortestPathNode = 'ShortestPathNode';
const SingletonNode = 'SingletonNode';
const SortNode = 'SortNode';
const SubqueryNode = 'SubqueryNode';
const TraversalNode = 'TraversalNode';
const UpdateNode = 'UpdateNode';
const UpsertNode = 'UpsertNode';
const nodeTypesList = [
CalculationNode, CollectNode, DistributeNode, EnumerateCollectionNode,
EnumerateListNode, EnumerateViewNode, FilterNode, GatherNode, IndexNode,
InsertNode, LimitNode, NoResultsNode, RemoteNode, RemoveNode, ReplaceNode,
ReturnNode, ScatterNode, ShortestPathNode, SingletonNode, SortNode,
SubqueryNode, TraversalNode, UpdateNode, UpsertNode
];
const CalculationBlock = 'CalculationNode';
const ConstrainedSortBlock = 'SortLimitNode';
const CountCollectBlock = 'CountCollectNode';
const DistinctCollectBlock = 'DistinctCollectNode';
const EnumerateCollectionBlock = 'EnumerateCollectionNode';
const EnumerateListBlock = 'EnumerateListNode';
const FilterBlock = 'FilterNode';
const HashedCollectBlock = 'HashedCollectNode';
const IndexBlock = 'IndexNode';
const LimitBlock = 'LimitNode';
const NoResultsBlock = 'NoResultsNode';
const RemoteBlock = 'RemoteNode';
const ReturnBlock = 'ReturnNode';
const ShortestPathBlock = 'ShortestPathNode';
const SingletonBlock = 'SingletonNode';
const SortBlock = 'SortNode';
const SortedCollectBlock = 'SortedCollectNode';
const SortingGatherBlock = 'SortingGatherNode';
const SubqueryBlock = 'SubqueryNode';
const TraversalBlock = 'TraversalNode';
const UnsortingGatherBlock = 'UnsortingGatherNode';
const RemoveBlock = 'RemoveNode';
const InsertBlock = 'InsertNode';
const UpdateBlock = 'UpdateNode';
const ReplaceBlock = 'ReplaceNode';
const UpsertBlock = 'UpsertNode';
const ScatterBlock = 'ScatterNode';
const DistributeBlock = 'DistributeNode';
const IResearchViewUnorderedBlock = 'IResearchUnorderedViewNode';
const IResearchViewBlock = 'IResearchViewNode';
const IResearchViewOrderedBlock = 'IResearchOrderedViewNode';
const blockTypesList = [
CalculationBlock, ConstrainedSortBlock, CountCollectBlock, DistinctCollectBlock,
EnumerateCollectionBlock, EnumerateListBlock, FilterBlock,
HashedCollectBlock, IndexBlock, LimitBlock, NoResultsBlock, RemoteBlock,
ReturnBlock, ShortestPathBlock, SingletonBlock, SortBlock,
SortedCollectBlock, SortingGatherBlock, SubqueryBlock, TraversalBlock,
UnsortingGatherBlock, RemoveBlock, InsertBlock, UpdateBlock, ReplaceBlock,
UpsertBlock, ScatterBlock, DistributeBlock, IResearchViewUnorderedBlock,
IResearchViewBlock, IResearchViewOrderedBlock
];
let translateType = function(nodes, node) {
let types = {};
nodes.forEach(function(node) {
let type = node.type;
if (type === 'CollectNode') {
if (node.collectOptions.method === 'sorted') {
type = 'SortedCollectNode';
} else if (node.collectOptions.method === 'hash') {
type = 'HashedCollectNode';
} else if (node.collectOptions.method === 'distinct') {
type = 'DistinctCollectNode';
} else if (node.collectOptions.method === 'count') {
type = 'CountCollectNode';
}
} else if (node.type === 'GatherNode') {
if (node.sortmode === 'minelement' || node.sortmode === 'heap') {
type = 'SortingGatherNode';
} else {
type = 'UnsortingGatherNode';
}
} else if (node.type === 'SortNode') {
if (node.strategy === 'standard') {
type = 'SortNode';
} else if (node.strategy === 'constrained-heap') {
type = 'SortLimitNode';
} else {
throw new Error('Unhandled sort strategy');
}
}
types[node.id] = type;
});
return types[node.id];
};
/// @brief check that numbers in actual are in the range specified by
/// expected. Each element in expected may either be
/// - a number, for an exact match;
/// - a range [min, max], to check if the actual element lies in this interval;
/// - null, so the actual element will be ignored
/// - undefined must be undefined
/// Also, the arrays must be of equal lengths.
function assertFuzzyNumArrayEquality(expected, actual, details) {
assert.assertEqual(expected.length, actual.length, details);
for (let i = 0; i < expected.length; i++) {
const exp = expected[i];
const act = actual[i];
if (exp === null) {
// do nothing
} else if ('number' === typeof exp) {
assert.assertEqual(exp, act, Object.assign({i}, details));
} else if ('undefined' === typeof exp) {
assert.assertEqual(exp, act, Object.assign({i}, details));
} else if (Array.isArray(exp)) {
assert.assertTrue(exp[0] <= act && act <= exp[1],
Object.assign(
{i, 'failed_test': `${exp[0]} <= ${act} <= ${exp[1]}`},
details
)
);
} else {
assert.assertTrue(false, {msg: "Logical error in the test code", exp});
}
}
}
function zipPlanNodesIntoStatsNodes (profile) {
const statsNodesById = profile.stats.nodes.reduce(
(accum, node) => {
accum[node.id] = node;
return accum;
},
{}
);
// Note: We need to take the order plan.nodes here, not stats.nodes,
// as stats.nodes is sorted by id.
return profile.plan.nodes.map(node => (
{
id: node.id,
type: translateType(profile.plan.nodes, node),
fromStats: statsNodesById[node.id], fromPlan: node
}
));
}
function getCompactStatsNodes (profile) {
// While we don't use any .fromPlan info here, zip uses the (correct) order
// of the plan, not from the stats (which is sorted by id).
return zipPlanNodesIntoStatsNodes(profile).map(
node => ({
type: translateType(profile.plan.nodes, node),
calls: node.fromStats.calls,
items: node.fromStats.items,
})
);
}
function getPlanNodesWithId (profile) {
return profile.plan.nodes.map(
node => ({
id: node.id,
type: node.type,
})
);
}
function getStatsNodesWithId (profile) {
return profile.stats.nodes.map(
node => ({
id: node.id,
type: node.type,
})
);
}
////////////////////////////////////////////////////////////////////////////////
/// @brief assert structure of profile.stats
////////////////////////////////////////////////////////////////////////////////
function assertIsProfileStatsObject (stats, {level, fullCount}) {
// internal argument check
expect(level)
.to.be.a('number')
.and.to.be.oneOf([0, 1, 2]);
expect(stats).to.be.an('object');
let statsKeys = [
'writesExecuted',
'writesIgnored',
'scannedFull',
'scannedIndex',
'filtered',
'httpRequests',
'peakMemoryUsage',
'executionTime',
];
if (level === 2) {
statsKeys.push('nodes');
}
if (fullCount) {
statsKeys.push('fullCount');
}
expect(stats).to.have.all.keys(statsKeys);
// check types
expect(stats.writesExecuted).to.be.a('number');
expect(stats.writesIgnored).to.be.a('number');
expect(stats.scannedFull).to.be.a('number');
expect(stats.scannedIndex).to.be.a('number');
expect(stats.filtered).to.be.a('number');
expect(stats.httpRequests).to.be.a('number');
expect(stats.peakMemoryUsage).to.be.a('number');
expect(stats.executionTime).to.be.a('number');
if (fullCount) {
expect(stats.fullCount).to.be.a('number');
}
}
////////////////////////////////////////////////////////////////////////////////
/// @brief assert structure of profile.warnings
////////////////////////////////////////////////////////////////////////////////
function assertIsProfileWarningsArray (warnings) {
expect(warnings).to.be.an('array');
// TODO check element type
}
////////////////////////////////////////////////////////////////////////////////
/// @brief assert structure of profile.profile
////////////////////////////////////////////////////////////////////////////////
function assertIsProfileProfileObject (profile) {
expect(profile).to.have.all.keys([
'initializing',
'parsing',
'optimizing ast',
'loading collections',
'instantiating plan',
'optimizing plan',
'executing',
'finalizing',
]);
expect(profile.initializing).to.be.a('number');
expect(profile.parsing).to.be.a('number');
expect(profile['optimizing ast']).to.be.a('number');
expect(profile['loading collections']).to.be.a('number');
expect(profile['instantiating plan']).to.be.a('number');
expect(profile['optimizing plan']).to.be.a('number');
expect(profile.executing).to.be.a('number');
expect(profile.finalizing).to.be.a('number');
}
////////////////////////////////////////////////////////////////////////////////
/// @brief assert structure of profile.plan
////////////////////////////////////////////////////////////////////////////////
function assertIsProfilePlanObject (plan) {
expect(plan).to.have.all.keys([
'nodes',
'rules',
'collections',
'variables',
'estimatedCost',
'estimatedNrItems',
'initialize',
'isModificationQuery',
]);
expect(plan.nodes).to.be.an('array');
expect(plan.rules).to.be.an('array');
expect(plan.collections).to.be.an('array');
expect(plan.variables).to.be.an('array');
expect(plan.estimatedCost).to.be.a('number');
expect(plan.estimatedNrItems).to.be.a('number');
expect(plan.initialize).to.be.a('boolean');
for (let node of plan.nodes) {
expect(node).to.include.all.keys([
'type',
'dependencies',
'id',
'estimatedCost',
'estimatedNrItems',
]);
expect(node.id).to.be.a('number');
expect(node.estimatedCost).to.be.a('number');
expect(node.estimatedNrItems).to.be.a('number');
expect(node.type)
.to.be.a('string')
.and.to.be.oneOf(nodeTypesList);
expect(node.dependencies)
.to.be.an('array');
// TODO add deep checks for plan.nodes[].dependencies
// TODO add checks for the optional variables, maybe dependent on the type
// e.g. for expression, inVariable, outVariable, canThrow, expressionType
// or elements...
}
// TODO add deep checks for plan.rules
// TODO add deep checks for plan.collections
for (let variable of plan.variables) {
expect(variable).to.have.all.keys([
'id',
'name',
]);
expect(variable.id).to.be.a('number');
expect(variable.name).to.be.a('string');
}
}
////////////////////////////////////////////////////////////////////////////////
/// @brief assert that the passed variable looks like a level 0 profile
////////////////////////////////////////////////////////////////////////////////
function assertIsLevel0Profile (profile, {fullCount} = {}) {
expect(profile)
.to.be.an('object')
.that.has.all.keys([
'stats',
'warnings',
]);
assertIsProfileStatsObject(profile.stats, {level: 0, fullCount});
assertIsProfileWarningsArray(profile.warnings);
}
////////////////////////////////////////////////////////////////////////////////
/// @brief assert that the passed variable looks like a level 1 profile
////////////////////////////////////////////////////////////////////////////////
function assertIsLevel1Profile (profile, {fullCount} = {}) {
expect(profile)
.to.be.an('object')
.that.has.all.keys([
'stats',
'warnings',
'profile',
]);
assertIsProfileStatsObject(profile.stats, {level: 1, fullCount});
assertIsProfileWarningsArray(profile.warnings);
assertIsProfileProfileObject(profile.profile);
}
////////////////////////////////////////////////////////////////////////////////
/// @brief assert that the passed variable looks like a level 2 profile
////////////////////////////////////////////////////////////////////////////////
function assertIsLevel2Profile (profile, {fullCount} = {}) {
expect(profile)
.to.be.an('object')
.that.has.all.keys([
'stats',
'warnings',
'profile',
'plan',
]);
assertIsProfileStatsObject(profile.stats, {level: 2, fullCount});
assertIsProfileWarningsArray(profile.warnings);
assertIsProfileProfileObject(profile.profile);
assertIsProfilePlanObject(profile.plan);
}
////////////////////////////////////////////////////////////////////////////////
/// @brief assert that the list of AQL nodes in the explain result matches the
/// list of AQL nodes of the profile.
////////////////////////////////////////////////////////////////////////////////
function assertStatsNodesMatchPlanNodes (profile) {
// Note: reorderings in the plan would break this comparison, because the
// stats are ordered by id. Thus we sort the nodes here.
assert.assertEqual(
profile.plan.nodes.map(node => node.id).sort(),
profile.stats.nodes.map(node => node.id).sort(),
{
'profile.plan.nodes': getPlanNodesWithId(profile),
'profile.stats.nodes': getStatsNodesWithId(profile),
}
);
}
////////////////////////////////////////////////////////////////////////////////
/// @brief Compares lists of nodes with items and calls, i.e., expected and
/// actual both have the structure [ { type, calls, items } ].
/// details may contain an object that will be output when the test fails,
/// maybe with additional fields.
/// .calls and .items may be either a number for an exact test, or a range
/// [min, max] which tests for min <= actualCalls <= max instead, or null to
/// ignore the value.
////////////////////////////////////////////////////////////////////////////////
function assertNodesItemsAndCalls (expected, actual, details = {}) {
// assert node types first
assert.assertEqual(
expected.map(node => node.type),
actual.map(node => node.type),
details
);
// assert item count second
assertFuzzyNumArrayEquality(
expected.map(node => node.items),
actual.map(node => node.items),
details
);
// assert call count last.
assertFuzzyNumArrayEquality(
expected.map(node => node.calls),
actual.map(node => node.calls),
details
);
}
////////////////////////////////////////////////////////////////////////////////
/// @brief Common checks for most blocks
/// @param query string - is assumed to have one bind parameter 'rows'
/// @param genNodeList function: (rows, batches) => [ { type, calls, items } ]
/// must generate the list of expected nodes
/// @param prepare function: (rows) => {...}
/// called before the query is executed
/// @param bind function: (rows) => ({rows})
/// must return the bind parameters for the query
/// Example for genNodeList:
/// genNodeList(2500, 3) ===
/// [
/// { type : SingletonNode, calls : 1, items : 1 },
/// { type : EnumerateListNode, calls : 3, items : 2500 },
/// { type : ReturnNode, calls : 3, items : 2500 }
/// ]
/// The number of calls may be a range [min, max], e.g.:
/// { type : EnumerateCollectionNode, calls : [3,5] , items : 2500 }
////////////////////////////////////////////////////////////////////////////////
function runDefaultChecks (
{
query,
genNodeList,
prepare = () => {},
bind = rows => ({rows}),
options = {},
testRowCounts = defaultTestRowCounts,
additionalTestRowCounts = [],
}
) {
const {fullCount} = options;
testRowCounts = _.uniq(testRowCounts.concat(additionalTestRowCounts).sort());
for (const rows of testRowCounts) {
prepare(rows);
const profile = db._query(query, bind(rows),
_.merge(options, {profile: 2, defaultBatchSize})
).getExtra();
assertIsLevel2Profile(profile, {fullCount});
assertStatsNodesMatchPlanNodes(profile);
const batches = Math.ceil(rows / defaultBatchSize);
const expected = genNodeList(rows, batches);
const actual = getCompactStatsNodes(profile);
assertNodesItemsAndCalls(expected, actual,
{query, bind: bind(rows), rows, batches, expected, actual});
}
}
////////////////////////////////////////////////////////////////////////////////
/// @brief Get an array of arrays of numbers. Each inner array is of length
/// numberOfShards. Specifies the number of rows per shard.
/// @param numberOfShards The number of shards
////////////////////////////////////////////////////////////////////////////////
function clusterTestRowCounts({numberOfShards}) {
const testRowCounts = [];
const empty = _.fill(Array(numberOfShards), 0);
// all shards empty
// e.g. [0, 0, 0, 0, 0]
let array = empty.slice();
testRowCounts.push(array.slice());
for (const rows of defaultClusterTestRowCounts) {
// first shard has "rows" documents
// e.g. [rows, 0, 0, 0, 0]
array = empty.slice();
array[0] = rows;
testRowCounts.push(array);
if (numberOfShards > 1) {
// last shard has "rows" documents
// e.g. [0, 0, 0, 0, rows]
array = empty.slice();
array[array.length-1] = rows;
testRowCounts.push(array);
}
// all shards have "rows" documents
// e.g. [rows, rows, rows, rows, rows]
array = _.fill(Array(numberOfShards), rows);
testRowCounts.push(array);
}
return testRowCounts;
}
////////////////////////////////////////////////////////////////////////////////
/// @brief Common checks for cluster tests
/// @param col ArangoCollection object
/// @param exampleDocumentsByShard Object, keys are shard ids and values are
/// arrays of documents which the specified shard is responsible for.
/// @param query string - is assumed to have no bind parameter
/// @param genNodeList function: (rowsByShard) => [ { type, calls, items } ]
/// must generate the list of expected nodes, gets a { shard => rows }
/// map (string to number).
////////////////////////////////////////////////////////////////////////////////
function runClusterChecks (
{
col,
exampleDocumentsByShard,
query,
genNodeList,
options = {},
}
) {
const numberOfShards = Object.keys(exampleDocumentsByShard).length;
const testRowCounts = clusterTestRowCounts({numberOfShards});
const prepareCollection = rowCounts => {
col.truncate();
let i = 0;
for (const [id, exampleDocs] of _.entries(exampleDocumentsByShard)) {
let num = rowCounts[i];
const docs = _.takeWhile(exampleDocs, () => num-- > 0);
col.insert(docs);
i++;
}
const countByShard = col.count(true);
assert.assertEqual(_.values(countByShard).sort(), rowCounts.slice().sort());
return countByShard;
};
for (const rowCounts of testRowCounts) {
const rowsByShard = prepareCollection(rowCounts);
const profile = db._query(query, {},
_.merge(options, {profile: 2, defaultBatchSize})
).getExtra();
assertIsLevel2Profile(profile);
assertStatsNodesMatchPlanNodes(profile);
const expected = genNodeList(rowsByShard);
const actual = getCompactStatsNodes(profile);
assertNodesItemsAndCalls(expected, actual,
{query: query, rowCounts, rowsByShard, expected, actual});
}
}
////////////////////////////////////////////////////////////////////////////////
/// @brief Fill the passed collections with a balanced binary tree.
/// All existing documents will be deleted in both collections!
/// @param vertexCol A document ArangoCollection
/// @param edgeCol An edge ArangoCollection
/// @param numVertices Number of vertices the binary tree should contain
////////////////////////////////////////////////////////////////////////////////
function createBinaryTree (vertexCol, edgeCol, numVertices) {
// clear collections
vertexCol.truncate();
edgeCol.truncate();
// add vertices
vertexCol.insert(_.range(1, numVertices + 1).map((i) => ({_key: ""+i})));
// create a balanced binary tree on edgeCol
edgeCol.insert(
// for every v._key from col
_.range(1, numVertices + 1)
// calculate child indices of (potential) children
.map(i => [{from: i, to: 2*i}, {from: i, to: 2*i+1}])
// flatten
.reduce((accum, cur) => accum.concat(cur), [])
// omit edges to non-existent vertices
.filter(e => e.to <= numVertices)
// create edge documents
.map(e => (
{
_from: `${colName}/${e.from}`,
_to: `${colName}/${e.to}`,
}
))
);
}
////////////////////////////////////////////////////////////////////////////////
/// @brief Adds a and b. Both may either be numbers or intervals of numbers,
/// modeled as two-element arrays. The result again is either a number
/// or an array of two numbers.
/// @param a A number or an array of two numbers
/// @param b A number or an array of two numbers
////////////////////////////////////////////////////////////////////////////////
//
function addIntervals(a, b) {
// helper
const assertInterval = (x) => {
assert.assertInstanceOf(Array, x);
assert.assertEqual(2, x.length);
assert.assertTypeOf("number", x[0]);
assert.assertTypeOf("number", x[1]);
};
// normalize arguments
if (typeof a === "number") {a = [a, a];}
if (typeof b === "number") {b = [b, b];}
assertInterval(a);
assertInterval(b);
// add intervals
let result = [a[0] + b[0], a[1] + b[1]];
assertInterval(result);
// normalize result
if (result[0] === result[1]) {
result = result[0];
}
return result;
}
exports.colName = colName;
exports.viewName = viewName;
exports.edgeColName = edgeColName;
exports.defaultBatchSize = defaultBatchSize;
exports.defaultTestRowCounts = defaultTestRowCounts;
exports.CalculationNode = CalculationNode;
exports.CollectNode = CollectNode;
exports.DistributeNode = DistributeNode;
exports.EnumerateCollectionNode = EnumerateCollectionNode;
exports.EnumerateListNode = EnumerateListNode;
exports.EnumerateViewNode = EnumerateViewNode;
exports.FilterNode = FilterNode;
exports.GatherNode = GatherNode;
exports.IndexNode = IndexNode;
exports.InsertNode = InsertNode;
exports.LimitNode = LimitNode;
exports.NoResultsNode = NoResultsNode;
exports.RemoteNode = RemoteNode;
exports.RemoveNode = RemoveNode;
exports.ReplaceNode = ReplaceNode;
exports.ReturnNode = ReturnNode;
exports.ScatterNode = ScatterNode;
exports.ShortestPathNode = ShortestPathNode;
exports.SingletonNode = SingletonNode;
exports.SortNode = SortNode;
exports.SubqueryNode = SubqueryNode;
exports.TraversalNode = TraversalNode;
exports.UpdateNode = UpdateNode;
exports.UpsertNode = UpsertNode;
exports.nodeTypesList = nodeTypesList;
exports.CalculationBlock = CalculationBlock;
exports.ConstrainedSortBlock = ConstrainedSortBlock;
exports.CountCollectBlock = CountCollectBlock;
exports.DistinctCollectBlock = DistinctCollectBlock;
exports.EnumerateCollectionBlock = EnumerateCollectionBlock;
exports.EnumerateListBlock = EnumerateListBlock;
exports.FilterBlock = FilterBlock;
exports.HashedCollectBlock = HashedCollectBlock;
exports.IndexBlock = IndexBlock;
exports.LimitBlock = LimitBlock;
exports.NoResultsBlock = NoResultsBlock;
exports.RemoteBlock = RemoteBlock;
exports.ReturnBlock = ReturnBlock;
exports.ShortestPathBlock = ShortestPathBlock;
exports.SingletonBlock = SingletonBlock;
exports.SortBlock = SortBlock;
exports.SortedCollectBlock = SortedCollectBlock;
exports.SortingGatherBlock = SortingGatherBlock;
exports.SubqueryBlock = SubqueryBlock;
exports.TraversalBlock = TraversalBlock;
exports.UnsortingGatherBlock = UnsortingGatherBlock;
exports.RemoveBlock = RemoveBlock;
exports.InsertBlock = InsertBlock;
exports.UpdateBlock = UpdateBlock;
exports.ReplaceBlock = ReplaceBlock;
exports.UpsertBlock = UpsertBlock;
exports.ScatterBlock = ScatterBlock;
exports.DistributeBlock = DistributeBlock;
exports.IResearchViewUnorderedBlock = IResearchViewUnorderedBlock;
exports.IResearchViewBlock = IResearchViewBlock;
exports.IResearchViewOrderedBlock = IResearchViewOrderedBlock;
exports.blockTypesList = blockTypesList;
exports.assertFuzzyNumArrayEquality = assertFuzzyNumArrayEquality;
exports.zipPlanNodesIntoStatsNodes = zipPlanNodesIntoStatsNodes;
exports.getCompactStatsNodes = getCompactStatsNodes;
exports.getPlanNodesWithId = getPlanNodesWithId;
exports.getStatsNodesWithId = getStatsNodesWithId;
exports.assertIsProfileStatsObject = assertIsProfileStatsObject;
exports.assertIsProfileWarningsArray = assertIsProfileWarningsArray;
exports.assertIsProfileProfileObject = assertIsProfileProfileObject;
exports.assertIsProfilePlanObject = assertIsProfilePlanObject;
exports.assertIsLevel0Profile = assertIsLevel0Profile;
exports.assertIsLevel1Profile = assertIsLevel1Profile;
exports.assertIsLevel2Profile = assertIsLevel2Profile;
exports.assertStatsNodesMatchPlanNodes = assertStatsNodesMatchPlanNodes;
exports.assertNodesItemsAndCalls = assertNodesItemsAndCalls;
exports.addIntervals = addIntervals;
exports.runDefaultChecks = runDefaultChecks;
exports.runClusterChecks = runClusterChecks;
exports.createBinaryTree = createBinaryTree;