in Clients/AmbrosiaJS/PTI-Node/App/src/Main.ts [39:370]
async function main()
{
const ONE_KB: number = 1024;
const ONE_MB: number = ONE_KB * ONE_KB;
const CLIENT_ROLE_NAME = PTI.InstanceRoles[PTI.InstanceRoles.Client];
const SERVER_ROLE_NAME = PTI.InstanceRoles[PTI.InstanceRoles.Server];
const COMBINED_ROLE_NAME = PTI.InstanceRoles[PTI.InstanceRoles.Combined];
const CLIENT_PARAMS: string[] = ["-sin", "--serverInstanceName", "-bpr", "--bytesPerRound", "-bsc", "--batchSizeCutoff", "-mms", "--maxMessageSize", "-n", "--numOfRounds", "-nds", "--noDescendingSize", "-fms", "--fixedMessageSize", "-eeb", "--expectedEchoedBytes", "-ipm", "--includePostMethod"];
const SERVER_PARAMS: string[] = ["-nhc", "--noHealthCheck", "-bd", "--bidirectional", "-efb", "--expectedFinalBytes"];
let instanceRole: string;
let serverInstanceName: string;
let bytesPerRound: number = 1024 * ONE_MB; // 1 GB
let batchSizeCutoff: number = 10 * ONE_MB; // 10 MB
let maxMessageSize: number = 64 * ONE_KB; // 64 KB
let numberOfRounds: number = 1;
let useDescendingSize: boolean;
let useFixedMessageSize: boolean;
let checkpointPadding: number = 0; // In bytes
let noHealthCheck: boolean;
let expectedFinalBytes: number = 0;
let expectedEchoedBytes: number = 0;
// Note: 'autoContinue' defaults to false in C# PTI. However, if we did the same we'd want to set --autoContinue in runClient.ps1/runServer.ps1 so that the user
// wouldn't have to explicitly add it as a parameter each time. But if we did that then the user would have no way to way to turn off --autoContinue.
// So instead we default it to true, and consume it as "=true|false" parameter. An alternative would be to rename it to --waitAtStart and default it to false.
let autoContinue: boolean = true;
let bidirectional: boolean = false;
let verifyPayload: boolean = false;
let includePostMethod: boolean = false;
/** [Local function] Returns the first command-line arg (if any) found in the supplied 'paramList', otherwise returns null. */
function getCommandLineArgIn(paramList: string[]): string | null
{
const args: string[] = Process.argv;
for (let i = 2; i < args.length; i++)
{
const paramName: string = args[i].split("=")[0];
if (paramList.indexOf(paramName) !== -1)
{
return (paramName);
}
}
return (null);
}
try
{
// Parse command-line parameters
try
{
if (Utils.hasCommandLineArg("-h|help"))
{
throw new Error("ShowHelp");
}
instanceRole = Utils.getCommandLineArg("-ir|instanceRole", COMBINED_ROLE_NAME); // JS-only
serverInstanceName = Utils.getCommandLineArg("-sin|serverInstanceName", ""); // JS-only
bytesPerRound = parseInt(Utils.getCommandLineArg("-bpr|bytesPerRound", bytesPerRound.toString())); // JS-only
batchSizeCutoff = parseInt(Utils.getCommandLineArg("-bsc|batchSizeCutoff", batchSizeCutoff.toString())); // JS-only
maxMessageSize = parseInt(Utils.getCommandLineArg("-mms|maxMessageSize", maxMessageSize.toString()));
numberOfRounds = parseInt(Utils.getCommandLineArg("-n|numOfRounds", numberOfRounds.toString()));
useFixedMessageSize = Utils.hasCommandLineArg("-fms|fixedMessageSize"); // JS-only
useDescendingSize = !Utils.hasCommandLineArg("-nds|noDescendingSize") && !useFixedMessageSize;
checkpointPadding = parseInt(Utils.getCommandLineArg("-m|memoryUsed", checkpointPadding.toString()));
noHealthCheck = Utils.hasCommandLineArg("-nhc|noHealthCheck"); // JS-only
expectedFinalBytes = parseInt(Utils.getCommandLineArg("-efb|expectedFinalBytes", expectedFinalBytes.toString())); // JS-only
autoContinue = Utils.equalIgnoringCase(Utils.getCommandLineArg("-c|autoContinue", autoContinue.toString()), "true");
bidirectional = Utils.hasCommandLineArg("-bd|bidirectional");
expectedEchoedBytes = parseInt(Utils.getCommandLineArg("-eeb|expectedEchoedBytes", expectedEchoedBytes.toString())); // JS-only
verifyPayload = Utils.hasCommandLineArg("-vp|verifyPayload"); // JS-only
includePostMethod = Utils.hasCommandLineArg("-ipm|includePostMethod"); // JS-only
const unknownArgName: string | null = Utils.getUnknownCommandLineArg();
if (unknownArgName)
{
throw new Error(`Invalid parameter: The supplied '${unknownArgName}' parameter is unknown; specify '--help' to see all possible parameters`);
}
// Validate parameters
const availableRoles: string[] = Utils.getEnumKeys("InstanceRoles", PTI.InstanceRoles);
if (availableRoles.indexOf(instanceRole) === -1)
{
throw new Error(`Invalid parameter: The supplied --instanceRole ('${instanceRole}') must be '${availableRoles.join("' or '")}'`);
}
// Check that all the supplied parameters are valid for the role
if ((instanceRole === CLIENT_ROLE_NAME) && getCommandLineArgIn(SERVER_PARAMS))
{
throw new Error(`Invalid parameter: The ${getCommandLineArgIn(SERVER_PARAMS)} parameter is only valid when --instanceRole is '${SERVER_ROLE_NAME}' (or '${COMBINED_ROLE_NAME}')`);
}
if ((instanceRole === SERVER_ROLE_NAME) && getCommandLineArgIn(CLIENT_PARAMS))
{
throw new Error(`Invalid parameter: The ${getCommandLineArgIn(CLIENT_PARAMS)} parameter is only valid when --instanceRole is '${CLIENT_ROLE_NAME}' (or '${COMBINED_ROLE_NAME}')`);
}
if ((instanceRole === CLIENT_ROLE_NAME) && !serverInstanceName)
{
throw new Error(`Missing parameter: The --serverInstanceName is required when --instanceRole is '${CLIENT_ROLE_NAME}'`);
}
const bytesPerRoundPower2: number = Math.log2(bytesPerRound);
if (!Number.isInteger(bytesPerRoundPower2) || (bytesPerRoundPower2 <= 6))
{
throw new Error(`Invalid parameter: The supplied --bytesPerRound (${bytesPerRound}) must be an exact power of 2 greater than 6 (128+)`);
}
const maxMessageSizePower2 = Math.log2(maxMessageSize);
if (!Number.isInteger(maxMessageSizePower2) || (maxMessageSizePower2 < 6) || (maxMessageSizePower2 > bytesPerRoundPower2))
{
throw new Error(`Invalid parameter: --maxMessageSize (${maxMessageSize}) must be set to an exact power of 2 between 6 (64) and ${bytesPerRoundPower2} (${Math.pow(2, bytesPerRoundPower2)}) [the --bytesPerRound value]`);
}
if (batchSizeCutoff > bytesPerRound)
{
throw new Error(`Invalid parameter: --batchSizeCutoff (${batchSizeCutoff}) must be less than or equal to --bytesPerRound (${bytesPerRound})`);
}
if (useDescendingSize && (numberOfRounds > 1) && ((numberOfRounds - 1) > (maxMessageSizePower2 - 6)))
{
// This is not an error condition, but the result may not be what the user expected so we emit a warning
console.log(`WARNING: The supplied --numOfRounds (${numberOfRounds}) is larger than needed to reach the 64 byte minimum message size when using "descending size"; the final ${numberOfRounds - (maxMessageSizePower2 - 6) - 1} rounds will use a message size of 64`);
}
const MAX_UINT32: number = Math.pow(2, 32) - 1; // The message ID is sent as a Uint32, so we can only send MAX_UINT32 messages in total (in all rounds)
const maxMessagesPerRound: number = (bytesPerRound / 64); // For simplicity, we assume the "worst" case (ie. all messages being 64 bytes)
const maxRounds: number = Math.floor(MAX_UINT32 / maxMessagesPerRound);
if ((numberOfRounds < 1) || (numberOfRounds > maxRounds))
{
// For example, if bytesPerRound is 1GB then maxRounds will be 255
throw new Error(`Invalid parameter: The supplied --numOfRounds (${numberOfRounds}) must be between 1 and ${maxRounds}`);
}
if (expectedFinalBytes > 0)
{
if ((instanceRole === COMBINED_ROLE_NAME) && (expectedFinalBytes != numberOfRounds * bytesPerRound))
{
throw new Error(`Invalid parameter: The supplied --expectedFinalBytes (${expectedFinalBytes}) should either be 0 or ${numberOfRounds * bytesPerRound}`);
}
// Validate that expectedFinalBytes is a multiple of some number that's a power of 2
// (eg. 256MB [Client #1] + 128MB [Client #2] = 384MB, which is a multiple of a 128MB so it's valid).
const expectedFinalBytesPower2: number = Math.log2(expectedFinalBytes);
if (!Number.isInteger(expectedFinalBytesPower2))
{
let isMultipleOfPowerOf2: boolean = false;
for (let powerOf2 = Math.floor(expectedFinalBytesPower2) - 1; powerOf2 > 0; powerOf2--)
{
if (expectedFinalBytes % Math.pow(2, powerOf2) === 0)
{
isMultipleOfPowerOf2 = true;
break;
}
}
if (!isMultipleOfPowerOf2)
{
throw new Error(`Invalid parameter: The supplied --expectedFinalBytes (${expectedFinalBytes}) must be a multiple of a number that is an exact power of 2`);
}
}
}
else
{
// If possible, set a default for expectedFinalBytes. Note that for the explicit 'Server' role there can be
// multiple clients, each with its own bytesPerRound and numberOfRounds, so we can't compute a default value.
if (instanceRole === COMBINED_ROLE_NAME)
{
expectedFinalBytes = numberOfRounds * bytesPerRound; // This will always be multiple of some number that's a power of 2
}
}
if ((instanceRole === CLIENT_ROLE_NAME) && (expectedEchoedBytes > 0) && (expectedEchoedBytes !== (numberOfRounds * bytesPerRound)))
{
throw new Error(`Invalid parameter: The supplied --expectedEchoedBytes (${expectedEchoedBytes}) should either be 0 or ${numberOfRounds * bytesPerRound}`);
}
if ((instanceRole === COMBINED_ROLE_NAME) && bidirectional)
{
if (expectedEchoedBytes === 0)
{
expectedEchoedBytes = expectedFinalBytes;
}
else
{
if (expectedEchoedBytes !== expectedFinalBytes)
{
throw new Error(`Invalid parameter: The supplied --expectedEchoedBytes (${expectedEchoedBytes}) must be the same as --expectedFinalBytes (${expectedFinalBytes}) when --bidirectional is specified in the '${COMBINED_ROLE_NAME}' role`);
}
}
}
const nodeMaxOldGenerationSize: number = Utils.getNodeLongTermHeapSizeInBytes(); // _appState will end up in the long-term ("old") GC heap
const maxCheckpointPadding: number = Math.floor((nodeMaxOldGenerationSize * 0.8) / ONE_MB) * ONE_MB; // Largest whole MB <= 80% of nodeMaxOldGenerationSize
if ((checkpointPadding < 0) || (checkpointPadding > maxCheckpointPadding))
{
throw new Error(`Invalid parameter: The supplied memoryUsed (${checkpointPadding}) must be between 0 and ${maxCheckpointPadding} (${maxCheckpointPadding / ONE_MB} MB); set the node.js V8 parameter '--max-old-space-size' to raise the upper limit (see https://nodejs.org/api/cli.html)`);
}
}
catch (e: unknown)
{
const error: Error = Utils.makeError(e);
console.log("");
if (error.message === "ShowHelp")
{
console.log(" PTI Parameters:");
console.log(" ===============");
console.log(" -h|--help : [Common] Displays this help message");
console.log(" -ir|--instanceRole= : [Common] The role of this instance in the test ('Server', 'Client', or 'Combined'); defaults to 'Combined'");
console.log(" -m|--memoryUsed= : [Common] Optional \"padding\" (in bytes) used to simulate large checkpoints by being included in app state; defaults to 0");
console.log(" -c|--autoContinue= : [Common] Whether to continue automatically at startup (if true), or wait for the 'Enter' key (if false); defaults to true");
console.log(" -vp|--verifyPayload : [Common] Enables verifying the message payload bytes (for 'doWork' on the server, and 'doWorkEcho' on the client); enabling this will decrease performance");
console.log(" -sin|--serverInstanceName= : [Client] The name of the instance that's acting in the 'Server' role for the test; only required when --role is 'Client'");
console.log(" -bpr|--bytesPerRound= : [Client] The total number of message payload bytes that will be sent in a single round; defaults to 1 GB");
console.log(" -bsc|--batchSizeCutoff= : [Client] Once the total number of message payload bytes queued reaches (or exceeds) this limit, then the batch will be sent; defaults to 10 MB");
console.log(" -mms|--maxMessageSize= : [Client] The maximum size (in bytes) of the message payload; must be a power of 2 (eg. 65536), and be at least 64; defaults to 64KB");
console.log(" -n|--numOfRounds= : [Client] The number of rounds (of size bytesPerRound) to work through; each round will use a [potentially] different message size; defaults to 1");
console.log(" -nds|--noDescendingSize : [Client] Disables descending (halving) the message size after each round; instead, a random size [power of 2] between 64 and --maxMessageSize will be used");
console.log(" -fms|--fixedMessageSize : [Client] All messages (in all rounds) will be of size --maxMessageSize; --noDescendingSize (if also supplied) will be ignored");
console.log(" -eeb|--expectedEchoedBytes= : [Client] The total number of \"echoed\" bytes expected to be received from the server when --bidirectional is specified; the client will report a \"success\" message when this number of bytes have been received");
console.log(" -ipm|--includePostMethod : [Client] Includes a 'post' method call in the test");
console.log(" -nhc|--noHealthCheck : [Server] Disables the periodic server health check (requested via an Impulse message)");
console.log(" -bd|--bidirectional : [Server] Enables echoing the 'doWork' method call back to the client(s)");
console.log(" -efb|--expectedFinalBytes= : [Server] The total number of bytes expected to be received from all clients; the server will report a \"success\" message when this number of bytes have been received");
}
else
{
console.log(error.message);
}
console.log("");
return;
}
// Run the app
await Ambrosia.initializeAsync();
const outputLoggingLevel: Utils.LoggingLevel = Configuration.loadedConfig().lbOptions.outputLoggingLevel;
if (outputLoggingLevel !== Utils.LoggingLevel.Minimal)
{
PTI.log(`Warning: Set the 'outputLoggingLevel' in ${Configuration.loadedConfigFileName()} to 'Minimal' (not '${Utils.LoggingLevel[outputLoggingLevel]}') for optimal performance`);
}
// Check if the effective batch size exceeds 50% of the "maxMessageQueueSizeInMB" config setting.
// Note: We use 50% (not 100%) because we only know the message payload size (maxMessageSize), not the actual on-the-wire message size, but at 50% we are guaranteed to be able to queue 1 batch.
const effectiveBatchSize: number = (maxMessageSize >= batchSizeCutoff) ? maxMessageSize : (Math.ceil(batchSizeCutoff / maxMessageSize) * maxMessageSize);
const maxMessageQueueSizeInBytes: number = Configuration.loadedConfig().lbOptions.maxMessageQueueSizeInMB * ONE_MB;
const maxEffectiveBatchSize: number = maxMessageQueueSizeInBytes / 2;
if (effectiveBatchSize > maxEffectiveBatchSize)
{
let maxBatchSizeCutoff: number = 0;
if (batchSizeCutoff > maxMessageSize)
{
if (maxEffectiveBatchSize % maxMessageSize === 0)
{
maxBatchSizeCutoff = maxEffectiveBatchSize;
}
else
{
maxBatchSizeCutoff = Math.floor(maxEffectiveBatchSize / maxMessageSize) * maxMessageSize;
}
}
console.log(`\nInvalid parameter: The effective batch size (${effectiveBatchSize}) cannot be larger than ${maxEffectiveBatchSize}; ` +
`reduce ${(maxMessageSize > batchSizeCutoff) ? `--maxMessageSize (to ${maxEffectiveBatchSize}` : `--batchSizeCutoff (to ${maxBatchSizeCutoff}`} or less) to resolve this\n`);
return;
}
// For the 'Server' or 'Combined' role, set serverInstanceName to the local instance name (for the 'Client' role, we've already checked that a --serverInstanceName was supplied)
if (!serverInstanceName)
{
serverInstanceName = IC.instanceName();
}
// Prevent a client instance from targeting itself as the server
if ((instanceRole === CLIENT_ROLE_NAME) && Utils.equalIgnoringCase(serverInstanceName, IC.instanceName()))
{
console.log(`\nInvalid parameter: When --instanceRole is '${CLIENT_ROLE_NAME}' the --serverInstanceName cannot reference the local instance ('${IC.instanceName()}'); instead, set --instanceRole to '${COMBINED_ROLE_NAME}'\n`);
return;
}
// The client sends its name in the message payload, which can be as small as 64 bytes, so the client name is limited to 59 bytes (+1 byte for name length, +4 bytes for call number = 64 bytes)
if (((instanceRole === CLIENT_ROLE_NAME) || (instanceRole === COMBINED_ROLE_NAME)) && (StringEncoding.toUTF8Bytes(IC.instanceName()).length > 59))
{
console.log(`\nThe client instance name ('${IC.instanceName()}') is too long; the maximum allowed length is 59 bytes`);
return;
}
PTI.log(`Local instance is running in the '${instanceRole}' PTI role`);
if (!autoContinue)
{
// For debugging we don't want to auto-continue, but for test automation we do
PTI.log(`Pausing execution of '${IC.instanceName()}'. Press 'Enter' to continue...`);
await Utils.consoleReadKeyAsync([Utils.ENTER_KEY]);
}
_config = new Configuration.AmbrosiaConfig(Framework.messageDispatcher, Framework.checkpointProducer, Framework.checkpointConsumer, PublishedAPI.postResultDispatcher);
PTI.State._appState = IC.start(_config, PTI.State.AppState);
// Preserve command-line parameters in app-state [so that they're available upon re-start, in which case these
// command-line parameter values will be ignored since they'll be overwritten when the checkpoint is restored]
// Note: 'autoContinue' is not included in app-state because it's used for debugging.
PTI.State._appState.instanceRole = PTI.InstanceRoles[instanceRole as keyof typeof PTI.InstanceRoles];
PTI.State._appState.serverInstanceName = serverInstanceName;
PTI.State._appState.bytesPerRound = bytesPerRound;
PTI.State._appState.batchSizeCutoff = batchSizeCutoff;
PTI.State._appState.maxMessageSize = maxMessageSize;
PTI.State._appState.numRounds = numberOfRounds;
PTI.State._appState.numRoundsLeft = numberOfRounds;
PTI.State._appState.useDescendingSize = useDescendingSize;
PTI.State._appState.useFixedMessageSize = useFixedMessageSize;
PTI.State._appState.noHealthCheck = noHealthCheck;
PTI.State._appState.bidirectional = bidirectional;
PTI.State._appState.expectedFinalBytesTotal = expectedFinalBytes;
PTI.State._appState.expectedEchoedBytesTotal = expectedEchoedBytes;
if (checkpointPadding > 0)
{
const ONE_HUNDRED_MB: number = 100 * ONE_MB;
PTI.State._appState.checkpointPadding = new Array<Uint8Array>();
let padding: Uint8Array = new Uint8Array(checkpointPadding % ONE_HUNDRED_MB);
PTI.State._appState.checkpointPadding.push(padding);
for (let i = 0; i < Math.floor(checkpointPadding / ONE_HUNDRED_MB); i++)
{
padding = new Uint8Array(ONE_HUNDRED_MB).fill(i + 1);
PTI.State._appState.checkpointPadding.push(padding);
}
}
PTI.State._appState.verifyPayload = verifyPayload;
PTI.State._appState.includePostMethod = includePostMethod;
}
catch (error: unknown)
{
Utils.tryLog(Utils.makeError(error));
}
}