Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/bson-bench/src/base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as BSON from 'bson';
import { readFileSync } from 'fs';
import { join } from 'path';
import { performance } from 'perf_hooks';
import * as process from 'process';

Expand Down Expand Up @@ -116,10 +117,12 @@ function run(bson: BSONLib | ConstructibleBSON, config: BenchmarkSpecification)

function listener(message: RunBenchmarkMessage) {
if (message.type === 'runBenchmark') {
const packageSpec = new Package(message.benchmark.library);
const packageSpec = new Package(message.benchmark.library, message.benchmark.installLocation);
let bson: BSONLib;
try {
bson = require(packageSpec.computedModuleName);
bson = require(
join(message.benchmark.installLocation, 'node_modules', packageSpec.computedModuleName)
);
} catch (error) {
reportErrorAndQuit(error as Error);
return;
Expand Down
22 changes: 13 additions & 9 deletions packages/bson-bench/src/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as cp from 'child_process';
import { once } from 'events';
import * as path from 'path';
import { join } from 'path';

import { exists } from './utils';

Expand All @@ -26,8 +26,10 @@ export class Package {
gitCommitish?: string;
// path to local library
localPath?: string;
installPath: string;

constructor(libSpec: string) {
constructor(libSpec: string, installPath: string) {
this.installPath = installPath;
let match: RegExpExecArray | null;
if ((match = NPM_PACKAGE_REGEX.exec(libSpec))) {
this.type = 'npm';
Expand All @@ -44,7 +46,8 @@ export class Package {
this.library = match[1] as 'bson' | 'bson-ext';

this.localPath = match[2];
this.computedModuleName = `${this.library}-local-${this.localPath.replaceAll(path.sep, '_')}`;
const cleanedLocalPath = this.localPath.replaceAll('/', '_').replaceAll('\\', '_');
this.computedModuleName = `${this.library}-local-${cleanedLocalPath}`;
} else {
throw new Error('unknown package specifier');
}
Expand All @@ -55,7 +58,7 @@ export class Package {
*/
check<B extends BSONLib>(): B | undefined {
try {
return require(this.computedModuleName);
return require(join(this.installPath, 'node_modules', this.computedModuleName));
} catch {
return undefined;
}
Expand Down Expand Up @@ -90,10 +93,10 @@ export class Package {
break;
}

const npmInstallProcess = cp.exec(
`npm install ${this.computedModuleName}@${source} --no-save`,
{ encoding: 'utf8', cwd: __dirname }
);
const npmInstallProcess = cp.exec(`npm install ${this.computedModuleName}@${source}`, {
encoding: 'utf8',
cwd: this.installPath
});

const exitCode: number = (await once(npmInstallProcess, 'exit'))[0];
if (exitCode !== 0) {
Expand Down Expand Up @@ -130,11 +133,12 @@ export type BenchmarkSpecification = {
/** Specifier of the bson or bson-ext library to be used. Can be an npm package, git repository or
* local package */
library: string;
installLocation?: string;
};

export interface RunBenchmarkMessage {
type: 'runBenchmark';
benchmark: BenchmarkSpecification;
benchmark: Omit<BenchmarkSpecification, 'installLocation'> & { installLocation: string };
}

export interface ResultMessage {
Expand Down
70 changes: 42 additions & 28 deletions packages/bson-bench/src/task.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type ChildProcess, fork } from 'child_process';
import { once } from 'events';
import { writeFile } from 'fs/promises';
import { mkdir, rm, writeFile } from 'fs/promises';
import { tmpdir } from 'os';
import * as path from 'path';

import {
Expand All @@ -11,25 +12,28 @@ import {
type PerfSendResult,
type ResultMessage
} from './common';
import { exists } from './utils';

/**
* An individual benchmark task that runs in its own Node.js process
*/
export class Task {
result: BenchmarkResult | undefined;
benchmark: BenchmarkSpecification;
benchmark: Omit<BenchmarkSpecification, 'installLocation'> & { installLocation: string };
taskName: string;
testName: string;
/** @internal */
children: ChildProcess[];
/** @internal */
hasRun: boolean;

static packageInstallLocation: string = path.join(tmpdir(), 'bsonBench');

constructor(benchmarkSpec: BenchmarkSpecification) {
this.result = undefined;
this.benchmark = benchmarkSpec;
this.children = [];
this.hasRun = false;
this.benchmark = { ...benchmarkSpec, installLocation: Task.packageInstallLocation };

this.taskName = `${path.basename(this.benchmark.documentPath, 'json')}_${
this.benchmark.operation
Expand Down Expand Up @@ -174,31 +178,41 @@ export class Task {

// install required modules before running child process as new Node processes need to know that
// it exists before they can require it.
const pack = new Package(this.benchmark.library);
if (!pack.check()) await pack.install();
// spawn child process
const child = fork(`${__dirname}/base`, {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
serialization: 'advanced'
});
child.send({ type: 'runBenchmark', benchmark: this.benchmark });
this.children.push(child);

// listen for results or error
const resultOrError: ResultMessage | ErrorMessage = (await once(child, 'message'))[0];

// wait for child to close
await once(child, 'exit');

this.hasRun = true;
switch (resultOrError.type) {
case 'returnResult':
this.result = resultOrError.result;
return resultOrError.result;
case 'returnError':
throw resultOrError.error;
default:
throw new Error('Unexpected result returned from child process');
if (!(await exists(Task.packageInstallLocation))) {
await mkdir(Task.packageInstallLocation);
}

try {
const pack = new Package(this.benchmark.library, Task.packageInstallLocation);
if (!pack.check()) await pack.install();
// spawn child process
const child = fork(`${__dirname}/base`, {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
serialization: 'advanced'
});
child.send({ type: 'runBenchmark', benchmark: this.benchmark });
this.children.push(child);

// listen for results or error
const resultOrErrorPromise = once(child, 'message');
// Wait for process to exit
const exit = once(child, 'exit');

const resultOrError: ResultMessage | ErrorMessage = (await resultOrErrorPromise)[0];
await exit;

this.hasRun = true;
switch (resultOrError.type) {
case 'returnResult':
this.result = resultOrError.result;
return resultOrError.result;
case 'returnError':
throw resultOrError.error;
default:
throw new Error('Unexpected result returned from child process');
}
} finally {
await rm(Task.packageInstallLocation, { recursive: true, force: true });
}
}
}
73 changes: 52 additions & 21 deletions packages/bson-bench/test/unit/common.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
import { expect } from 'chai';
import { sep } from 'path';
import { mkdir, rm } from 'fs/promises';
import { tmpdir } from 'os';
import { join, sep } from 'path';

import { Package } from '../../lib/common';
import { clearTestedDeps } from '../utils';
import { clearTestedDeps, exists } from '../utils';

describe('common functionality', function () {
const BSON_PATH = process.env.BSON_PATH;

context('Package', function () {
beforeEach(clearTestedDeps);
after(clearTestedDeps);
let installDir: string;

after(async function () {
await rm(installDir, { recursive: true, force: true });
});

beforeEach(async function () {
await clearTestedDeps(installDir);
});

before(async function () {
installDir = join(tmpdir(), 'bsonBenchTest');
await mkdir(installDir);
});

context('constructor()', function () {
//github.com/mongodb-js/dbx-js-tools/pull/24/files
context('when given a correctly formatted npm package', function () {
it('sets computedModuleName correctly', function () {
const pack = new Package('[email protected]');
const pack = new Package('[email protected]', installDir);
expect(pack).to.haveOwnProperty('computedModuleName', 'bson-6.0.0');
});
});

context('when given a correctly formatted git repository', function () {
it('sets computedModuleName correctly', function () {
const pack = new Package('bson#eb98b8c39d6d5ba4ce7231ab9e0f29495d74b994');
const pack = new Package('bson#eb98b8c39d6d5ba4ce7231ab9e0f29495d74b994', installDir);
expect(pack).to.haveOwnProperty(
'computedModuleName',
'bson-git-eb98b8c39d6d5ba4ce7231ab9e0f29495d74b994'
Expand All @@ -31,13 +46,16 @@ describe('common functionality', function () {

context('when trying to install an npm package apart from bson or bson-ext', function () {
it('throws an error', function () {
expect(() => new Package('[email protected]')).to.throw(Error, /unknown package specifier/);
expect(() => new Package('[email protected]', installDir)).to.throw(
Error,
/unknown package specifier/
);
});
});

context('when trying to install a git package apart from bson or bson-ext', function () {
it('throws an error', function () {
expect(() => new Package('notBson#abcdabcdabcd')).to.throw(
expect(() => new Package('notBson#abcdabcdabcd', installDir)).to.throw(
Error,
/unknown package specifier/
);
Expand All @@ -50,7 +68,7 @@ describe('common functionality', function () {
console.log('Skipping since BSON_PATH is undefined');
this.skip();
}
const pack = new Package(`bson:${BSON_PATH}`);
const pack = new Package(`bson:${BSON_PATH}`, installDir);
expect(pack).to.haveOwnProperty(
'computedModuleName',
`bson-local-${BSON_PATH.replaceAll(sep, '_')}`
Expand All @@ -62,14 +80,14 @@ describe('common functionality', function () {
context('#check()', function () {
context('when package is not installed', function () {
it('returns undefined', function () {
const pack = new Package('bson@6');
const pack = new Package('bson@6', installDir);
expect(pack.check()).to.be.undefined;
});
});

context('when package is installed', function () {
it('returns the module', async function () {
const pack = new Package('[email protected]');
const pack = new Package('[email protected]', installDir);
await pack.install();
expect(pack.check()).to.not.be.undefined;
});
Expand All @@ -79,26 +97,31 @@ describe('common functionality', function () {
context('#install()', function () {
context('when given a correctly formatted npm package that exists', function () {
for (const lib of ['[email protected]', '[email protected]', 'bson@latest', 'bson-ext@latest']) {
it(`installs ${lib} successfully`, async function () {
const pack = new Package(lib);
it(`installs ${lib} successfully to the specified install directory`, async function () {
const pack = new Package(lib, installDir);
await pack.install();

expect(await exists(join(installDir, 'node_modules', pack.computedModuleName))).to.be
.true;
});
}
});

context('when given a correctly formatted npm package that does not exist', function () {
it('throws an error', async function () {
const bson9000 = new Package('bson@9000');
const bson9000 = new Package('bson@9000', installDir);
const error = await bson9000.install().catch(error => error);
expect(error).to.be.instanceOf(Error);
});
});

context('when given a correctly formatted git package using commit that exists', function () {
it('installs successfully', async function () {
const bson6Git = new Package('bson#58c002d');
it('installs successfully to specified install directory', async function () {
const bson6Git = new Package('bson#58c002d', installDir);
const maybeError = await bson6Git.install().catch(error => error);
expect(maybeError).to.be.undefined;
expect(await exists(join(installDir, 'node_modules', bson6Git.computedModuleName))).to.be
.true;
});
});

Expand All @@ -107,7 +130,10 @@ describe('common functionality', function () {
function () {
// TODO: NODE-6361: Unskip and fix this test.
it.skip('throws an error', async function () {
const bson6Git = new Package('bson#58c002d87bca9bbe7c7001cc6acae54e90a951bcf');
const bson6Git = new Package(
'bson#58c002d87bca9bbe7c7001cc6acae54e90a951bcf',
installDir
);
const maybeError = await bson6Git.install().catch(error => error);
expect(maybeError).to.be.instanceOf(Error);
});
Expand All @@ -118,9 +144,11 @@ describe('common functionality', function () {
'when given a correctly formatted git package using git tag that exists',
function () {
it('installs successfully', async function () {
const bson6Git = new Package('bson#v6.0.0');
const bson6Git = new Package('bson#v6.0.0', installDir);
const maybeError = await bson6Git.install().catch(error => error);
expect(maybeError).to.be.undefined;
expect(await exists(join(installDir, 'node_modules', bson6Git.computedModuleName))).to
.be.true;
});
}
);
Expand All @@ -129,7 +157,7 @@ describe('common functionality', function () {
'when given a correctly formatted git package using git tag that does not exist',
function () {
it('throws an error', async function () {
const bson6Git = new Package('bson#v999.999.9');
const bson6Git = new Package('bson#v999.999.9', installDir);
const maybeError = await bson6Git.install().catch(error => error);
expect(maybeError).to.be.instanceOf(Error);
});
Expand All @@ -143,16 +171,19 @@ describe('common functionality', function () {
this.skip();
}

const bsonLocal = new Package(`bson:${BSON_PATH}`);
const bsonLocal = new Package(`bson:${BSON_PATH}`, installDir);
const maybeError = await bsonLocal.install().catch(error => error);
expect(maybeError).to.not.be.instanceOf(Error, maybeError.message);
expect(await exists(join(installDir, 'node_modules', bsonLocal.computedModuleName))).to.be
.true;
});
});

context('when given a path that does not exist', function () {
it('throws an error', async function () {
const bsonLocal = new Package(
`bson:/highly/unlikely/path/to/exist/that/should/point/to/bson`
`bson:/highly/unlikely/path/to/exist/that/should/point/to/bson`,
installDir
);
const maybeError = await bsonLocal.install().catch(error => error);
expect(maybeError).to.be.instanceOf(Error);
Expand Down
Loading
Loading