Home

Awesome

njs-memory-profiler

This is a small tool designed to allow you understand the per-request memory usage of your njs script in a non-production environment.

This library is under active development and the interface may change without notice.

TODO:

Installation

This library can be installed using the normal npm workflow:

npm install njs-memory-profiler

This module will install to a folder called njs_modules in the root of your project.

Note: As the installation will be performed by an npm postinstall script. NodeJS version 16 or greater is required.

We recommend using asdf or nvm to manage your node versions.

If using nvm, just run nvm install from the root of the project. If using asdf, just run asdf install from the root of the project.

Usage

Assume we have a basic setup like this:

main.mjs

function hello(r) {
  r.return(200, "hello");
}

export default { hello };

nginx.conf

events {}

error_log /tmp/error.log debug;

http {
  js_import main from main.mjs;

  server {
    listen 4000;

    location / {
      js_content main.hello;
    }
  }
}

First, include the profiler_vars.conf file like this anywhere in the http context:

http {
    include njs_modules/njs-memory-profiler/conf/profiler_vars.conf;
    # < OTHER CONFIG >
}

Next, import the package, and initialize the profiler in the javascript file:

main.mjs

import profiler from "./njs_modules/njs-memory-profiler/njs-memory-profiler.mjs";

function hello(r) {
  profiler.init(r);
  r.return(200, "Hello");
}

export default { hello };

By default, per-request memory information will be written to the error log (in this case, /tmp/error.log). It is not necessary to call profiler.collect yourself unless you are going to use "Access Log Reporting" (see below)

Reporting Options

Error Log Reporting

By default, the profiler will simply log some json to the error log at the end of each request. This is the default behavior. Invoking the profiler as described in "Usage" will have this effect.

Access Log Reporting

The library provides nginx configuration files that can be used to set up additional variables and provides a log format.

To enable additional variables:

include njs_modules/njs-memory-profiler/conf/profiler_extra_vars.conf;

Using the provided variables you may set up your own log or use the provided format:

include njs_modules/njs-memory-profiler/conf/profiler_log_format.conf;
# Or if you'd prefer JSON in the logs
include njs_modules/njs-memory-profiler/conf/profiler_log_format_json.conf;

access_log /my/log/location/profiler.log profiler;

:warning: You MUST call collect explicitly when using this strategy. Since usually the profiler reports on the njs exit event, you must call the collect function with this reporter explicitly in the last part of your njs script because access logs are written before that event:

import profiler from "./njs-memory-profiler.mjs";

// Pass `null` for the reporter on init since you
// will be calling `collect` yourself later.
profiler.init(r, null);
// your code
profiler.collect(r, profiler.varReporter);
r.return(200, "We made it!");

File Reporting

import profiler from "./njs-memory-profiler.mjs";

profiler.init(r, profiler.fileReporter);

will write files to the current directory. The filename is in the format <request_id>.json

Custom reporting

If log-based or file-based reporting isn't what you need, you can provide a function that will receive the report.

The function will be passed the report as well as the njs request object shown as r in the example below.

To understand the format of the report object, see "Interpreting the Data" below.

To pass a handler:

profiler.init(r, (report, r) => {
  // Your custom reporting
  // Do not use async operations in this context
);

Note that the exit hook happens right before the njs vm for the request is destroyed. Long-running work may have strange consequences. Async operations will allow the vm to shut down and work will not be completed

Measuring Memory at Points

At any point after you initialize the profiler, you can take a snapshot of the memory state at a certain point:

import profiler from "./njs-memory-profiler.mjs";

function hello(r) {
  profiler.init(r);
  // ... do things
  profiler.pushEvent("event_name");
  r.return(200, "Hello");
}

export default { hello };

Interpreting the data

See the annotated example of output below:

{
  // A random id generated by the profiler to tie together events for this request
  "id": "cc7da804b5a8fdd9b803a87965cde018",
  // The `events` key contains all the actual profiling events
  "events": [
    {
      // There are three types of events: `profiler:start`, `profiler:snapshot`, `profiler:end`
      "type": "profiler:start",

      // Arbitrary name for the event.  For start and end they will default to
      // `profiler:start` and `profiler:end`
      "name": "profiler:start",

      // Unix timestamp for the event. In this case, when the profiler was initialized
      "createdAt": 1671144443913,

      // Size in bytes allocated to the njs vm at this point
      "size": 47600,

      // Number of blocks of memory allocated to the njs vm at this point
      "nblocks": 3
    },
    {
      "type": "profiler:snapshot",
      "name": "main_func",
      "createdAt": 1671144443913,
      "size": 47600,
      "nblocks": 3
    },
    {
      "type": "profiler:snapshot",
      "name": "js_var",
      "createdAt": 1671144443913,
      "size": 47600,
      "nblocks": 3
    },
    {
      "name": "profiler:end",
      "type": "profiler:end",
      "createdAt": 1671144443913,
      "size": 47600,
      "nblocks": 3
    }
  ]
}

Profiling overhead

There is a small amount of overhead from the profiler, however it is smaller than one "block" of memory so adding the profiler won't make a difference in your baseline number. However you will roll over to the next block more quickly. For any measurements, assume that you have a variance of page_size.

Interpreting memory growth

Njs pre-allocates memory and then continues to preallocate more in "nblocks" of page_size bytes. This means that it's possible to add code that will certainly use more memory, but size may not change because njs is working within its preallocated memory footprint already.

Profiling Backends

As part of its operation the profile needs to save some information for the duration of the request. By default, this data will be saved in njs variables.

The choice of backend will not affect how you instrument your code - but it could be useful if you find that the profiler overhead is too great.

NGINX Variables (default)

Profiling snapshots are condensed

[TODO] NGINX key-value store (NGINX Plus only)

[TODO] Memcached

[TODO] Redis

Directory Structure and Files

.
├── conf    <----- NGINX configuration files
├── package-lock.json
├── package.json
├── scripts <----- internal scripts used by the library.
└── src     <----- Njs-compatible Javascript sources

Contributing

Please see the contribution guide