|
| 1 | +import { fork } from 'node:child_process'; |
| 2 | +import { on, once } from 'node:events'; |
| 3 | +import { readFile, unlink, writeFile } from 'node:fs/promises'; |
| 4 | +import * as path from 'node:path'; |
| 5 | + |
| 6 | +import { expect } from 'chai'; |
| 7 | +import { parseSnapshot } from 'v8-heapsnapshot'; |
| 8 | + |
| 9 | +import { MongoClient } from '../../mongodb'; |
| 10 | +import { TestConfiguration } from '../../tools/runner/config'; |
| 11 | + |
| 12 | +export type ResourceTestFunction = (options: { |
| 13 | + MongoClient: typeof MongoClient; |
| 14 | + uri: string; |
| 15 | + iteration: number; |
| 16 | +}) => Promise<void>; |
| 17 | + |
| 18 | +const RESOURCE_SCRIPT_PATH = path.resolve(__dirname, '../../tools/fixtures/resource_script.in.js'); |
| 19 | +const DRIVER_SRC_PATH = JSON.stringify(path.resolve(__dirname, '../../../lib')); |
| 20 | + |
| 21 | +export async function testScriptFactory( |
| 22 | + name: string, |
| 23 | + uri: string, |
| 24 | + iterations: number, |
| 25 | + func: ResourceTestFunction |
| 26 | +) { |
| 27 | + let resourceScript = await readFile(RESOURCE_SCRIPT_PATH, { encoding: 'utf8' }); |
| 28 | + |
| 29 | + resourceScript = resourceScript.replace('DRIVER_SOURCE_PATH', DRIVER_SRC_PATH); |
| 30 | + resourceScript = resourceScript.replace('FUNCTION_STRING', `(${func.toString()})`); |
| 31 | + resourceScript = resourceScript.replace('NAME_STRING', JSON.stringify(name)); |
| 32 | + resourceScript = resourceScript.replace('URI_STRING', JSON.stringify(uri)); |
| 33 | + resourceScript = resourceScript.replace('ITERATIONS_STRING', `${iterations}`); |
| 34 | + |
| 35 | + return resourceScript; |
| 36 | +} |
| 37 | + |
| 38 | +/** |
| 39 | + * A helper for running arbitrary MongoDB Driver scripting code in a resource information collecting script |
| 40 | + * |
| 41 | + * **The provided function is run in an isolated Node.js process** |
| 42 | + * |
| 43 | + * A user of this function will likely need to familiarize themselves with the surrounding scripting, but briefly: |
| 44 | + * - Every MongoClient you construct should have an asyncResource attached to it like so: |
| 45 | + * ```js |
| 46 | + * mongoClient.asyncResource = new this.async_hooks.AsyncResource('MongoClient'); |
| 47 | + * ``` |
| 48 | + * - You can perform any number of operations and connects/closes of MongoClients |
| 49 | + * - The result of this function will be: |
| 50 | + * - the startup and teardown memory usage |
| 51 | + * - the number of AsyncResources with type === 'MongoClient' that did not get cleaned up by a destroy hook |
| 52 | + * - the heap snapshot parsed by 'v8-heapsnapshot' |
| 53 | + * |
| 54 | + * @param name - the name of the script, this defines the name of the file, it will be cleaned up if the function returns successfully |
| 55 | + * @param config - `this.configuration` from your mocha config |
| 56 | + * @param func - your javascript function, you can write it inline! this will stringify the function, use the references on the `this` context to get typechecking |
| 57 | + * @param options - settings for the script |
| 58 | + * @throws Error - if the process exits with failure |
| 59 | + */ |
| 60 | +export async function runScript( |
| 61 | + name: string, |
| 62 | + config: TestConfiguration, |
| 63 | + func: ResourceTestFunction, |
| 64 | + { iterations = 100 } = {} |
| 65 | +) { |
| 66 | + const scriptName = `${name}.cjs`; |
| 67 | + const heapsnapshotFile = `${name}.heapsnapshot.json`; |
| 68 | + |
| 69 | + const scriptContent = await testScriptFactory(name, config.url(), iterations, func); |
| 70 | + await writeFile(scriptName, scriptContent, { encoding: 'utf8' }); |
| 71 | + |
| 72 | + const processDiedController = new AbortController(); |
| 73 | + const script = fork(scriptName, { execArgv: ['--expose-gc'] }); |
| 74 | + // Interrupt our awaiting of messages if the process crashed |
| 75 | + script.once('close', exitCode => { |
| 76 | + if (exitCode !== 0) { |
| 77 | + processDiedController.abort(new Error(`process exited with: ${exitCode}`)); |
| 78 | + } |
| 79 | + }); |
| 80 | + |
| 81 | + const messages = on(script, 'message', { signal: processDiedController.signal }); |
| 82 | + const willClose = once(script, 'close'); |
| 83 | + |
| 84 | + const starting = await messages.next(); |
| 85 | + const ending = await messages.next(); |
| 86 | + |
| 87 | + const startingMemoryUsed = starting.value[0].startingMemoryUsed; |
| 88 | + const endingMemoryUsed = ending.value[0].endingMemoryUsed; |
| 89 | + |
| 90 | + // make sure the process ended |
| 91 | + const [exitCode] = await willClose; |
| 92 | + expect(exitCode, 'process should have exited with zero').to.equal(0); |
| 93 | + |
| 94 | + const heap = await readFile(heapsnapshotFile, { encoding: 'utf8' }).then(c => |
| 95 | + parseSnapshot(JSON.parse(c)) |
| 96 | + ); |
| 97 | + |
| 98 | + // If any of the above throws we won't reach these unlinks that clean up the created files. |
| 99 | + // This is intentional so that when debugging the file will still be present to check it for errors |
| 100 | + await unlink(scriptName); |
| 101 | + await unlink(heapsnapshotFile); |
| 102 | + |
| 103 | + return { |
| 104 | + startingMemoryUsed, |
| 105 | + endingMemoryUsed, |
| 106 | + heap |
| 107 | + }; |
| 108 | +} |
0 commit comments