npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

rameter

v0.12.3

Published

With Rameter, you can write load test with simple NodeJS scripts!

Downloads

3

Readme

Rameter

With Rameter, you can write load test with simple NodeJS scripts!

npm version

Table of Contents


About the Project

What is Rameter?

Rameter is a NodeJS based HTTP endpoint/API load test/performance test tool inspired by Jmeter, Locust, K6.

The name "Rameter" do come from Ram(from AOC) + (J)meter.

Why do I make Rameter?

  • I like the code-driven way of Locust & K6.
  • I like the web dashboard UI of Locust
  • I like the detail HTML test report generated by Jmeter.
  • I like the idea that we can write load test in JS like K6 do, but I want native Javascript library support.

Why Rameter?

  • Use it if you want to write some simple nodeJS script to do load test to your API.
  • Use it if you are using Jmeter, and you feel difficult about how to write dynamic logics with the testing endpoints.
  • Use it if you are using Locust, but you want to generate pretty HTML test report like Jmeter do.
  • Use it if you are using K6, but you want to use NodeJS native libraries, and you do not need the ultra performance of K6.

Features

  • Write NodeJS code to define multiple user groups & dynamic scenarios.
  • Calling HTTP request sampler with Axios wrapper.
  • Realtime web dashboard UI during test running.
  • Distributed mode.
  • Support Jmeter format CSV output, hence you can use Jmeter to generate HTML report.

Getting Started

Install

npm i rameter

Hello World

helloWorld.js

import { Rameter, HttpRequestSampler, sleep, UserModelConfig } from "rameter";

const rameter = new Rameter();

/**
 * up-lifting
 */
await rameter.runStandalone();

/**
 * define user groups
 */
rameter.defineUserGroup("members");

/**
 * define user test scenario
 */
rameter.defineUserScenario("hello scenario", async ({ globalContext, nodeContext, userContext, userId }) => {
  try {
    // call an HTTP endpoint
    await HttpRequestSampler.request({
      method: "GET",
      url: `http://localhost:8080/hello`, // replace this with ur endpoint
    });
  } catch (e) {
    console.error(e);
  } finally {
    await sleep(100); // think time
  }
});

await rameter.readyTest(); // Get ready to test

/**
 * Start the load test. Run until test finished.
 */
await rameter.runUntilFinished([
  {
    userModel: UserModelConfig.stepUserForTime({
      maxUsers: 20,
      steps: 10,
      rampUpTime: 10,
      holdTime: 20,
    }),
    userGroups: ["members"],
    scenarios: ["hello scenario"],
  },
]);

/**
 * shutdown
 */
await rameter.down();
process.exit();

Steps to run:

  1. Install rameter.
  2. Add the hello world script.
  3. Just a reminder. The hello world script is using ES module. So you may need to have your package.json defined with ... "type" : "module", .... Or otherwise you need to change the module import syntax.
  4. Replace the "hello scenario" part to call your endpoint.
  5. Run the script.
  6. You will then see the console is logging that it is calling the endpoint.
  7. It will also auto open the web dashboard to show real-time graphs.

Concepts

Component Diagram

Component Diagram

  • Master: Serve as controller. It fire commands to worker nodes to run the test. It collect result statistics.

  • Worker Node: The actual component to run the test. There can be N worker nodes to scale out test client side capacity.

  • Web Dashboard: Realtime web dashboard to view test statistics.

Shared variable context

  • Global Context: Context variable to globally shared in the whole test scope.

  • User Context: Context variable to share within user scope.

  • Node Context: Each worker node can load local resources (e.g: large file) and share within the same node scope.

Realtime Dashboard Demo Screen

Realtime dashboard

It dynamically shows:

  • Average response time per API over time
  • Throughput per API over time
  • API successful rate per API over time
  • User Group number of users over time

Docs

User Models

stepUserForTime

  1. Ramp up M users.
  2. Each User would loop the scenario infinitely.
  3. After all users are spawned, the test would end after holding for N seconds.
await rameter.runUntilFinished([
  {
    userModel: UserModelConfig.stepUserForTime({
      maxUsers: 20,
      rampUpTime: 10,
      steps: 10,
      holdTime: 60,
      delayTime: 0
    }),
    userGroups: ["members"],
    scenarios: ["hello scenario"],
  },
]);

Options

| Option | Description | Mandatory? | Default Value | |------------|--------------------------------------------------------|------------|---------------| | maxUsers | The maximum number of users you want to spawn. | Yes | - | | steps | The users would be spawn with this number of steps. | Yes | - | | rampUpTime | The number of seconds to ramp up all users. | Yes | - | | holdTime | The number of seconds to hold after all users spawned. | Yes | - | | delayTime | The number of seconds to wait before the user ramp up. | No | 0 |


stepUserLoopN

  1. Ramp up M users.
  2. Each User would loop the scenario for N times.
  3. After all users finished the loops, the test would be ended.
await rameter.runUntilFinished([
  {
    userModel: UserModelConfig.stepUserLoopN({
      maxUsers: 20,
      steps: 10,
      rampUpTime: 10,
      runNTimes: 100,
      delayTime: 0
    }),
    userGroups: ["members"],
    scenarios: ["hello scenario"],
  },
]);

Options

| Option | Description | Mandatory? | Default Value | |------------|--------------------------------------------------------|------------|---------------| | maxUsers | The maximum number of users you want to spawn. | Yes | - | | steps | The users would be spawn with this number of steps. | Yes | - | | rampUpTime | The number of seconds to ramp up all users. | Yes | - | | runNTimes | Each user would run the scenario for N times. | No | 1 | | delayTime | The number of seconds to wait before the user ramp up. | No | 0 |


userLoopForTime

  1. Spawn M users.
  2. Each User would loop the scenario infinitely.
  3. The test would end after holding for N seconds.
await rameter.runUntilFinished([
  {
    userModel: UserModelConfig.userLoopForTime({
      maxUsers: 20,
      holdTime: 60,
      delayTime: 0
    }),
    userGroups: ["members"],
    scenarios: ["hello scenario"],
  },
]);

Options

| Option | Description | Mandatory? | Default Value | |------------|--------------------------------------------------------|------------|---------------| | maxUsers | The maximum number of users you want to spawn. | Yes | - | | holdTime | The number of seconds to hold after all users spawned. | Yes | - | | delayTime | The number of seconds to wait before the user ramp up. | No | 0 |


userLoopN

  1. Spawn M users.
  2. Each User would loop the scenario for N times.
  3. After all users finished the loops, the test would be ended.
await rameter.runUntilFinished([
  {
    userModel: UserModelConfig.userLoopN({
      maxUsers: 20,
      holdTime: 60,
      delayTime: 0
    }),
    userGroups: ["members"],
    scenarios: ["hello scenario"],
  },
]);

Options

| Option | Description | Mandatory? | Default Value | |------------|--------------------------------------------------------|------------|---------------| | maxUsers | The maximum number of users you want to spawn. | Yes | - | | runNTimes | Each user would run the scenario for N times. | No | 1 | | delayTime | The number of seconds to wait before the user ramp up. | No | 0 |


Calling HTTP request

TLDR: The HttpRequestSampler.request method is a light wrapper for Axios request client.

const response = await HttpRequestSampler.request({
  method: "GET",
  url: `http://localhost:8080/hello`,
});

or

const response = await HttpRequestSampler.request({
  label: "hello API",
  method: "GET",
  url: `http://localhost:3000/hello`,
});

What the HttpRequestSampler.request actually is:

  1. Having a optional label option to group HTTP APIs. Otherwise we would use {HTTP method + URL path} as the label.
  2. Default to config that all HTTP status code interpret as not error.
  3. All remaining options are passed to Axios to call the HTTP request.
  4. Collect the time taken and success state statistics.
  5. Return back the response object for your own usage.

Custom Sampler

What if you want to verify operation performance which is not HTTP request call? For examples, loading DB operation? Schedule job processing time?

Actually Rameter provide you the CustomSampler which you can run any code fragment as an operation. Rameter would measure the time to complete it just like it do for HTTP requests.

Inside your scenario, you can write like this.

// use CustomSampler to measure some operation
await CustomSampler.run(async () => {
  await sleep(Math.round(Math.random() * 2000));
  return {
    data: { foo: 1 },
    sampleRecord: {
      label: "Mocking some operation",
    },
  };
});

Notice the code function passed to the CustomSampler.run. It is expected to be an async function, which return { data: XXX, sampleRecord: YYY }:

  • XXX is any data result that you may want to return to scenario.
  • YYY is test metadata. It is optional and usually you don't need it but it is recommended that you at least define label attribute which can group sampler result.

You can see an full example in Custom Sampler.


API

Rameter class

This is the major class to use to control the test.

Import:

import { Rameter } from "rameter";

For run the standalone setup (Both Master & Worker Node is in local)

/**
 * Run the setup for standalone mode.
 * It would create a Master & a local Workder Node
 *
 * @param config
 * @returns
 */
runStandalone(config: MasterConfig & WorkerNodeConfig): Promise<{
    master: Master;
    workerNode: WorkerNode;
}>;

For run the distributed mode's Master and Worker Node

/**
 * Run the setup for distribute mode - Run the Master
 *
 * @param config
 * @returns
 */
runMaster(config: MasterConfig): Promise<Master>;
/**
 * Run the setup for distribute mode - Run the Worker Node
 *
 * @param config
 * @returns
 */
runWorkerNode(config: WorkerNodeConfig): Promise<WorkerNode>;

Let the Master broadcast Global Context to Worker Nodes.

/**
 * Export the globalContext. It would broadcase to all connecting Workder Node
 *
 * @param globalContext
 */
exportGlobalContext(globalContext: any): Promise<void | undefined>;

(it must be run after called runStandalone or runMaster)


Define a handler to load Node Context. This is for the case that you want every Workder Node doing some operation to load Worker Node local shared variables.

/**
 * Set the handler for loading Node Context.
 *
 * @param loadNodeContextHandler
 */
loadNodeContext(loadNodeContextHandler: (globalContext: any) => Promise<any>): Promise<void | undefined>;

Notes:

  • You should define this loadNodeContext in your Workder Node, coz it is run on Worker Node side.
  • The node context handler is optional. You may no need to call this if you don't use Node Context.

Define user group

/**
 * Define User Group.
 *
 * @param name
 * @param initUserContext
 */
defineUserGroup(name: string, initUserContext: InitUserContextFunction): Promise<void | undefined>;

declare type InitUserContextFunction = (parameters: {
    globalContext: any;
    nodeContext: any;
    userId: string;
}) => Promise<any>;

Define user scenario

/**
 * Define User Scenario.
 *
 * @param name
 * @param scenario
 */
defineUserScenario(name: string, scenario: Scenario): Promise<void | undefined>;

declare type Scenario = (parameters: {
    globalContext: any;
    nodeContext: any;
    userContext: any;
    userId: string;
}) => Promise<void>;

The above two define function (it must be run after called runStandalone or runMaster)


Let the Workder Node to declare ready for start testing

/**
 * Mark for Work Node ready for start testing.
 *
 * @returns
 */
readyTest(): Promise<void | undefined>;

Notes:

  • This must be called after you defined all users, scenarios.
  • This must be called in your Workder Node.
  • Without calling this, the Master would not able to start testing becoz it is waiting worker nodes to notify ready.

Start running the test, and wait until test finished

/**
 * Start running the test, and wait until test finished
 *
 * @param options
 * @param param1
 */
runUntilFinished(options: RunTestOptions[], { log, intermediateSummary, tickSummary, openDashboard, }?: {
    log?: boolean;
    openDashboard?: boolean;
}): Promise<void>;

Noted:

  • log: it control whether to console out summary logs for every some seconds during test running. Default true.
  • openDashboard: it control whether auto open a web dashboard to view real-time statistics. Default true.

Shut down Master & Workder Nodes

down(): Promise<void>;

Shut down Worker Nodes

downNodes(): Promise<void>;

Util for Transform output file to Jmeter CSV format out file

transformJmeterOut(inFile: string, outFile: string): Promise<void>;

Full configuration

Sample:

{
    "master": {
        "host": "localhost",
        "port": 3001
    },
    "numWorkerNodes": 1,
    "fileResultStore": {
        "outFile": "./result.csv",
        "batchRecordThershold": 100,
        "batchTimeThershold": 5000
    },
    "sampleForwarder": {
        "batchRecordThershold": 100,
        "batchTimeThershold": 5000
    },
    "dashboard": {
        "origin": "http://localhost:3000",
        "port": 3000
    }
}

| Option | Description | Mandatory? | Default Value | |------------|--------------------------------------------------------|------------|---------------| | master.host | The host of Master. This is the host which workder nodes can resolve to connect | No | localhost | | master.port | The port of Master. | No | 3001 | | numWorkerNodes | The number of Worker Nodes. | No but recommend explicitly set | 1 | | fileResultStore.outFile | The csv file to write test samples. | No | "./result.csv" | | fileResultStore.batchRecordThershold | The store would flush if buffered N records | No | 100 | | fileResultStore.batchTimeThershold | The store would flush if passed N milleseconds | No | 5000 | | sampleForwarder.batchRecordThershold | The forwarder would flush if buffered N records | No | 100 | | sampleForwarder.batchTimeThershold | The forwarder would flush if passed N milleseconds | No | 5000 | | dashboard.origin | The dashboard website origin | No | "http://localhost:3000" | | dashboard.port | The dashboard port | No | 3000 |


Samples