Skip to content

Commit

Permalink
Merge pull request #1 from moxious/v0.3.0
Browse files Browse the repository at this point in the history
V0.3.0
  • Loading branch information
moxious authored Mar 14, 2019
2 parents 5f5e49c + 39d4e58 commit 2d9e2e4
Show file tree
Hide file tree
Showing 29 changed files with 312 additions and 82 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ See the `workloads` directory for the format of the probability table.
You can use the script `npm run graph-workload` as a synonym for running the index.js file, but keep in mind npm requires an extra `--` argument prior to passing
program arguments, as in, `npm run graph-workload -- --n 20`

# Examples

Create a lot of nodes as fast as possible:

```
npm run graph-workload -- -a localhost -u neo4j -p admin --query 'Unwind range(1,1000000) as id create (n);' -n 50 --concurrency 4
```

# Tests

```
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "src/run-workload.js",
"scripts": {
"graph-workload": "node src/run-workload.js",
"test": "nyc --reporter=lcov --reporter=text-lcov mocha --recursive"
"test": "nyc --reporter=lcov mocha --recursive"
},
"repository": {
"type": "git",
Expand Down
34 changes: 34 additions & 0 deletions src/SimpleQueryStrategy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const Strategy = require('./Strategy');

/**
* Represents a container class for a strategy that is just running some
* simple query with no setup.
*/
module.exports = class SimpleQueryStrategy extends Strategy {
constructor(props) {
super(props);
this.name = props.name || 'SimpleQuery';
this.query = props.query;
this.params = props.params || {};

if (!(props.mode === 'READ') && !(props.mode === 'WRITE')) {
throw new Error('Mode must be READ or WRITE');
}

this.mode = props.mode;
}

run() {
const f = (s) => {
const txRunner = tx => tx.run(this.query, this.params);

if (this.props.mode === 'READ') {
return s.readTransaction(txRunner);
}

return s.writeTransaction(txRunner);
};

return this.time(f);
}
}
2 changes: 1 addition & 1 deletion src/Strategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Strategy {
}

getName() { return this.name; }
run(driver) {
run() {
return Promise.reject('Override me in subclass');
}

Expand Down
4 changes: 2 additions & 2 deletions src/read-strategy/AggregateReadStrategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ class AggregateReadStrategy extends Strategy {
this.name = 'AggregateRead';
}

run(driver) {
const f = (s = driver.session()) => s.readTransaction(tx => tx.run(`
run() {
const f = (s) => s.readTransaction(tx => tx.run(`
MATCH (v:NAryTree)
WHERE id(v) % $r = 0
RETURN min(v.val), max(v.val), stdev(v.val), count(v.val)`,
Expand Down
4 changes: 2 additions & 2 deletions src/read-strategy/LongPathReadStrategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ class LongPathReadStrategy extends Strategy {
this.name = 'LongPathRead';
}

run(driver) {
run() {
const start = 1 + this.randInt(1000);

const f = (s = driver.session()) => s.readTransaction(tx => tx.run(`
const f = (s) => s.readTransaction(tx => tx.run(`
MATCH p=(s:NAryTree { val: $start })-[r:child*]->(e:NAryTree { val: $end })
RETURN count(r)`,
{ start, end: start + this.randInt(500) }));
Expand Down
2 changes: 1 addition & 1 deletion src/read-strategy/MetadataReadStrategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class MetadataReadStrategy extends Strategy {
this.name = 'MetadataRead';
}

run(driver) {
run() {
const i = this.randInt(50);

const f = (s) => {
Expand Down
4 changes: 2 additions & 2 deletions src/read-strategy/RandomAccessReadStrategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ class RandomAccessReadStrategy extends Strategy {
this.limit = props.limit || 50;
}

run(driver) {
run() {
const nth = Math.floor(Math.random() * this.prime) + 1;
const skip = Math.floor(Math.random() * 1000) + 1;

const f = (s = driver.session()) => s.readTransaction(tx => tx.run(`
const f = (s) => s.readTransaction(tx => tx.run(`
MATCH (node)
WHERE id(node) % $prime = $nth
RETURN keys(node)
Expand Down
98 changes: 74 additions & 24 deletions src/run-configuration.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const terminateAfter = require('./termination-condition');
const ProbabilityTable = require('./stats/ProbabilityTable');
const _ = require('lodash');

const usageStr = `
Usage: run-workload.js -p password
Expand All @@ -8,11 +9,13 @@ Usage: run-workload.js -p password
[-n hits] how many total queries to run
[--ms milliseconds] how many milliseconds to test for
[--workload /path/to/workload.json] probability table spec
[--query CYPHER_QUERY] single cypher query to run
[--concurrency c] how many concurrent queries to run (default: 10)
[--checkpoint cn] how often to print results in milliseconds (default: 5000)
[--fail-fast] if specified, the work will stop after encountering one failure.
You may only specify one of the options --n or --ms.
You may only specify one of the options --workload or --query
`;

const defaultProbabilityTable = [
Expand All @@ -28,21 +31,11 @@ const defaultProbabilityTable = [
[1, 'rawWrite'],
];

const generateFromArgs = (args) => {
const badlyConfigured = (
// User is being inconsistent about when to stop.
(args.n && args.ms) ||
// We don't know where to connect...
(!process.env.NEO4J_URI && !args.a) ||
// Don't know what password to use...
(!process.env.NEO4J_PASSWORD && !args.p)
);

if (badlyConfigured) {
usage();
}

const probabilityTable = args.workload ? require(args.workload) : defaultProbabilityTable;
/**
* @param {*} args a yargs object
* @returns { iterateUntil, runType } of when to stop.
*/
const chooseTerminationType = (args) => {
let iterateUntil;
let runType;

Expand All @@ -58,25 +51,81 @@ const generateFromArgs = (args) => {
runType = 'counted';
}

const p = Number(args.concurrency) || Number(process.env.CONCURRENCY);

const failFast = ('fail-fast' in args) ? args['fail-fast'] : false;
return { iterateUntil, runType };
};

/**
* @param {*} args a yargs object
* @returns { username, password, address } of where to connect
*/
const chooseConnectionDetails = (args) => {
const addressify = str =>
str.indexOf('://') === -1 ? `bolt://${str}` : str;

const obj = {
return {
username: args.u || process.env.NEO4J_USER || 'neo4j',
password: args.p || process.env.NEO4J_PASSWORD,
address: addressify(args.a || process.env.NEO4J_URI),
probabilityTable: new ProbabilityTable(probabilityTable),
runType,
checkpointFreq: args.checkpoint || process.env.CHECKPOINT_FREQUENCY || 5000,
};
};

const chooseConcurrency = (args) => {
const p = Number(args.concurrency) || Number(process.env.CONCURRENCY);
return {
concurrency: (!Number.isNaN(p) && p > 0) ? p : 10,
iterateUntil,
};
};

const chooseProbabilityTable = (args) => {
let ptData = args.workload ? require(args.workload) : defaultProbabilityTable;

if (args.query) {
// Always run the custom query.
ptData = [
[ 1.0, 'custom' ],
];
}

const result = {
probabilityTable: new ProbabilityTable(ptData)
};

if (args.query) {
result.query = args.query;
}

return result;
};

const generateFromArgs = (args) => {
const badlyConfigured = (
// User is being inconsistent about when to stop.
(args.n && args.ms) ||
// Trying to specify to both run a single query and a different workload...
(args.query && args.workload) ||
// We don't know where to connect...
(!process.env.NEO4J_URI && !args.a) ||
// Don't know what password to use...
(!process.env.NEO4J_PASSWORD && !args.p)
);

if (badlyConfigured) {
usage();
}

const terminationType = chooseTerminationType(args);
const connectionDetails = chooseConnectionDetails(args);
const concurrency = chooseConcurrency(args);
const probabilityTable = chooseProbabilityTable(args);

const failFast = ('fail-fast' in args) ? args['fail-fast'] : false;

// Merge sub-objects.
const obj = _.merge({
checkpointFreq: args.checkpoint || process.env.CHECKPOINT_FREQUENCY || 5000,
failFast,
phase: 'NOT_STARTED',
};
}, terminationType, probabilityTable, connectionDetails, concurrency);

if (obj.runType === 'counted') {
obj.n = args.n || 10000;
Expand All @@ -101,6 +150,7 @@ module.exports = {
.describe('n', 'number of hits on the database')
.describe('ms', 'number of milliseconds to execute')
.describe('workload', 'absolute path to JSON probability table/workload')
.describe('query', 'Cypher query to run')
.default('concurrency', 10)
.default('checkpoint', 5000)
.demandOption(['p'])
Expand Down
6 changes: 6 additions & 0 deletions src/stats/ProbabilityTable.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const _ = require('lodash');

/**
* Represents a table of probabilities, and different chosen outcomes.
* A table is an array of arrays, where the first cell is a float from 0-1,
Expand Down Expand Up @@ -35,6 +37,10 @@ module.exports = class ProbabilityTable {
}
}

getLabels() {
return _.uniq(this.data.map(row => row[1]));
}

choose() {
const roll = Math.random();

Expand Down
71 changes: 52 additions & 19 deletions src/strategies.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* Library of strategies for easy inclusion.
*/
const SimpleQueryStrategy = require('./SimpleQueryStrategy');
const NAryTreeStrategy = require('./write-strategy/NAryTreeStrategy');
const FatNodeAppendStrategy = require('./write-strategy/FatNodeAppendStrategy');
const MergeWriteStrategy = require('./write-strategy/MergeWriteStrategy');
Expand All @@ -16,28 +17,60 @@ const LongPathReadStrategy = require('./read-strategy/LongPathReadStrategy');
const RandomAccessReadStrategy = require('./read-strategy/RandomAccessReadStrategy');

/**
* Produces a strategy table
* Produces a strategy table by looking through the probability table and assigning
* only those strategies that are needed.
* @param {SessionPool} sessionPool
* @param {runConfig} a runconfig object
* @returns {Object} key is strategy name, value is a Strategy instance object.
*/
const builder = sessionPool => ({
// WRITE STRATEGIES
naryWrite: new NAryTreeStrategy({ n: 2, sessionPool }),
fatnodeWrite: new FatNodeAppendStrategy({ sessionPool }),
mergeWrite: new MergeWriteStrategy({ n: 1000000, sessionPool }),
rawWrite: new RawWriteStrategy({ n: 10, sessionPool }),
randomLinkage: new RandomLinkageStrategy({ n: 1000000, sessionPool }),
starWrite: new StarWriteStrategy({ sessionPool }),
indexHeavy: new IndexHeavyStrategy({ sessionPool }),
lockTorture: new LockTortureStrategy({ sessionPool }),
luceneWrite: new LuceneWriteStrategy({ sessionPool }),
const builder = (sessionPool, runConfig) => {
const defaultStratTable = {
// WRITE STRATEGIES
naryWrite: new NAryTreeStrategy({ n: 2, sessionPool }),
fatnodeWrite: new FatNodeAppendStrategy({ sessionPool }),
mergeWrite: new MergeWriteStrategy({ n: 1000000, sessionPool }),
rawWrite: new RawWriteStrategy({ n: 10, sessionPool }),
randomLinkage: new RandomLinkageStrategy({ n: 1000000, sessionPool }),
starWrite: new StarWriteStrategy({ sessionPool }),
indexHeavy: new IndexHeavyStrategy({ sessionPool }),
lockTorture: new LockTortureStrategy({ sessionPool }),
luceneWrite: new LuceneWriteStrategy({ sessionPool }),

// READ STRATEGIES
aggregateRead: new AggregateReadStrategy({ sessionPool }),
metadataRead: new MetadataReadStrategy({ sessionPool }),
longPathRead: new LongPathReadStrategy({ sessionPool }),
randomAccess: new RandomAccessReadStrategy({ sessionPool }),
});
// READ STRATEGIES
aggregateRead: new AggregateReadStrategy({ sessionPool }),
metadataRead: new MetadataReadStrategy({ sessionPool }),
longPathRead: new LongPathReadStrategy({ sessionPool }),
randomAccess: new RandomAccessReadStrategy({ sessionPool }),
};

if (runConfig) {
// By choosing which strats are in the table (by probability table)
// We're limiting which setup methods get run too.
const labels = runConfig.probabilityTable.getLabels();
const chosenTable = {};

labels.forEach(label => {
const strat = defaultStratTable[label];

if (label === 'custom') {
chosenTable[label] = new SimpleQueryStrategy({
sessionPool,
query: runConfig.query,
mode: runConfig.mode || 'WRITE',
});
return;
} else if(!strat) {
throw new Error(`Probability table references strat ${label} which is unknown`);
}

chosenTable[label] = strat;
});

return chosenTable;
}

return defaultStratTable;
};

/**
* Reports key stats about strategies
Expand Down Expand Up @@ -66,5 +99,5 @@ const showLastQuery = strategyTable => {
};

module.exports = {
builder, report, showLastQuery,
builder, report, showLastQuery,
};
2 changes: 1 addition & 1 deletion src/workload.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class Workload {
neo4j.auth.basic(this.runConfig.username, this.runConfig.password));

this.sessionPool = pool.getPool(this.driver, this.runConfig.concurrency);
this.strategyTable = strategies.builder(this.sessionPool);
this.strategyTable = strategies.builder(this.sessionPool, this.runConfig);
this.stats = new WorkloadStats(this.runConfig);

this.promisePool = new PromisePool(promiseProducer(this), this.runConfig.concurrency);
Expand Down
3 changes: 3 additions & 0 deletions src/workloads/nary-only.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
[ 1.0, "naryWrite" ]
]
Loading

0 comments on commit 2d9e2e4

Please sign in to comment.