Skip to content

Commit

Permalink
chore(sdk): add SDK reproducers
Browse files Browse the repository at this point in the history
  • Loading branch information
basti1302 committed Nov 8, 2022
1 parent 25fcb4b commit 4782f33
Show file tree
Hide file tree
Showing 14 changed files with 360 additions and 1 deletion.
23 changes: 23 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
"graphql-v14": "npm:[email protected]",
"graphql-v16": "npm:[email protected]",
"graphql-ws": "^5.5.5",
"heapdump": "^0.3.15",
"husky": "^7.0.4",
"ibm_db": "^2.8.2",
"ioredis": "^4.28.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
SDK Memory Leak Reproducer
==========================

This directory contains a couple of small test applications that try to reproduce a memory leak in the SDK, or rather, in our use of AsyncLocalStorage/cls-hooked.

They all create an entry span in regular intervals via the SDK. They do not need to be triggered by an external request.

Available Reproducers
---------------------

There are currently six different reproducers, two for each SDK API style:
* async,
* promise, and
* callback.

One of each pair is using a *recursive* call pattern. That is, the next call is triggered from the context of the previous call. The call is triggerd via `setTimeout` but that does not matter with respect to AsyncLocalStorage (ALS for short)/cls-hooked, because the context is kept across `setTimeout` (or any async mechanism – this is precisely the point of ALS/cls-hooked).

The other reproducer is scheduling regular calls via `setInterval`. The crucial difference is that calls are not triggered from the context of the preceding call, but from the root context.

The current hypothesis is that all recursive reproducers are affected by the leak, no matter which API style the use or whether they use ALS or the legacy cls-hooked implementation. Additionally, some of the non-recursive reproducers might also be affected due to the failure of existing the context after Namespace#runPromise.

Usage
-----
The SDK will only actually create spans when @instana/collector has established a connection to an agent. To run the examples, you therefore need to start an agent locally.

If you are not interested in inspecting the reported data in Instana, you can start
```
DROP_DATA=true node packages/collector/test/apps/agentStub
```

in a separate shell. Otherwise, start an Instana agent locally. Be aware that the reproducers will create a lot of spans, though.

Start a reproducer like this:

node packages/collector/test/tracing/sdk/memory_leak_reproducer/async_recursive.js

There are a couple of options to control the behavior:

```
# Set a custom delay in milliseconds between individual calls. The default is currently 10 (!) milliseconds.
DELAY=1000 node packages/collector/test/tracing/sdk/memory_leak_reproducer/async_recursive.js
# Force @instana/collector to use the legacy cls-hooked library instead of AsyncLocalStorage.
INSTANA_FORCE_LEGACY_CLS=true node packages/collector/test/tracing/sdk/memory_leak_reproducer/async_recursive.js
# Print additional debug output
DEBUG_CLS=true node packages/collector/test/tracing/sdk/memory_leak_reproducer/async_recursive.js
```

### Creating a Heapdump

All reproducers load the `heapdump` module. Execute `kill -USR2 $pid` to create a heapdump while the process is running. Heapdumps can be inspected via Chrome/Chromium for example (DevTools -> tab memory -> Load).

Analysis
--------

The detailed analysis is currently not available in publicly. The internal link is: https://www.notion.so/instana/SDK-Memory-Leak-a807a1b242c84976ac79d9a1a9037494
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env node
/*
* (c) Copyright IBM Corp. 2022
*/

'use strict';

const instana = require('../../../..')({
serviceName: require('path').basename(__filename)
});

// eslint-disable-next-line no-unused-vars
const heapdump = require('heapdump');
const { delayBetweenCalls, simulateWork } = require('./util');

async function createSdkSpan() {
await instana.sdk.async.startEntrySpan('span-name');
await simulateWork();
instana.sdk.async.completeEntrySpan();
}

async function trigger() {
await createSdkSpan();
setTimeout(trigger, delayBetweenCalls);
}

setTimeout(trigger, 3000);
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env node
/*
* (c) Copyright IBM Corp. 2022
*/

'use strict';

const instana = require('../../../..')({
serviceName: require('path').basename(__filename)
});

// eslint-disable-next-line no-unused-vars
const heapdump = require('heapdump');
const { delayBetweenCalls, simulateWork } = require('./util');

async function createSdkSpan() {
await instana.sdk.async.startEntrySpan('span-name');
await simulateWork();
for (let i = 0; i < 2; i++) {
// eslint-disable-next-line no-await-in-loop
await createIntermediateSpanWithExitChildren();
// eslint-disable-next-line no-await-in-loop
await simulateWork();
}
instana.sdk.async.completeEntrySpan();
}

async function createIntermediateSpanWithExitChildren() {
await instana.sdk.async.startIntermediateSpan('span-name-intermediate');
await simulateWork();
for (let i = 0; i < 2; i++) {
// eslint-disable-next-line no-await-in-loop
await createExitSpan();
// eslint-disable-next-line no-await-in-loop
await simulateWork();
}
instana.sdk.async.completeIntermediateSpan();
}

async function createExitSpan() {
await instana.sdk.async.startExitSpan('span-name-exit');
await simulateWork();
instana.sdk.async.completeExitSpan();
}

async function trigger() {
await createSdkSpan();
if (process.env.DEBUG_CLS) {
process._rawDebug('----');
}
setTimeout(trigger, delayBetweenCalls);
}

setTimeout(trigger, 3000);
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env node
/*
* (c) Copyright IBM Corp. 2022
*/

'use strict';

const instana = require('../../../..')({
serviceName: require('path').basename(__filename)
});

// eslint-disable-next-line no-unused-vars
const heapdump = require('heapdump');
const { delayBetweenCalls, simulateWork } = require('./util');

async function createSdkSpan() {
await instana.sdk.async.startEntrySpan('span-name');
await simulateWork();
instana.sdk.async.completeEntrySpan();
}

async function trigger() {
await createSdkSpan();
}

setInterval(trigger, delayBetweenCalls);
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env node
/*
* (c) Copyright IBM Corp. 2022
*/

'use strict';

const instana = require('../../../..')({
serviceName: require('path').basename(__filename)
});

// eslint-disable-next-line no-unused-vars
const heapdump = require('heapdump');
const { delayBetweenCalls, simulateWorkCallback } = require('./util');

function createSdkSpan(cb) {
instana.sdk.callback.startEntrySpan('span-name', () => {
simulateWorkCallback(() => {
instana.sdk.callback.completeEntrySpan();
cb();
});
});
}

async function trigger() {
createSdkSpan(() => {
setTimeout(trigger, delayBetweenCalls);
});
}

setTimeout(trigger, 3000);
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env node
/*
* (c) Copyright IBM Corp. 2022
*/

'use strict';

const instana = require('../../../..')({
serviceName: require('path').basename(__filename)
});

// eslint-disable-next-line no-unused-vars
const heapdump = require('heapdump');
const { delayBetweenCalls, simulateWorkCallback } = require('./util');

function createSdkSpan() {
instana.sdk.callback.startEntrySpan('span-name', () => {
simulateWorkCallback(() => {
instana.sdk.callback.completeEntrySpan();
});
});
}

async function trigger() {
createSdkSpan();
}

setInterval(trigger, delayBetweenCalls);
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env node
/*
* (c) Copyright IBM Corp. 2022
*/

'use strict';

const instana = require('../../../..')({
serviceName: require('path').basename(__filename)
});

// eslint-disable-next-line no-unused-vars
const heapdump = require('heapdump');
const { delayBetweenCalls, simulateWork } = require('./util');

function createSdkSpan() {
return instana.sdk.promise.startEntrySpan('span-name').then(() => {
simulateWork().then(() => {
instana.sdk.promise.completeEntrySpan();
});
});
}

function trigger() {
createSdkSpan().then(() => {
setTimeout(trigger, delayBetweenCalls);
});
}

setTimeout(trigger, 3000);
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env node
/*
* (c) Copyright IBM Corp. 2022
*/

'use strict';

const instana = require('../../../..')({
serviceName: require('path').basename(__filename)
});

// eslint-disable-next-line no-unused-vars
const heapdump = require('heapdump');
const { delayBetweenCalls, simulateWork } = require('./util');

function createSdkSpan() {
return instana.sdk.promise.startEntrySpan('span-name').then(() => {
simulateWork().then(() => {
instana.sdk.promise.completeEntrySpan();
});
});
}

function trigger() {
createSdkSpan();
}

setInterval(trigger, delayBetweenCalls);
26 changes: 26 additions & 0 deletions packages/collector/test/tracing/sdk/memory_leak_reproducer/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* (c) Copyright IBM Corp. 2022
*/

'use strict';

const defaultDelayBetweenCalls = 10;

exports.delayBetweenCalls = defaultDelayBetweenCalls;
if (process.env.DELAY) {
exports.delayBetweenCalls = parseInt(process.env.DELAY, 10);
if (Number.isNaN(exports.delayBetweenCalls)) {
exports.delayBetweenCalls = defaultDelayBetweenCalls;
}
}

// eslint-disable-next-line no-console
console.log(`delay between calls: ${exports.delayBetweenCalls}`);

exports.simulateWork = function simulateWork(ms = 2) {
return new Promise(resolve => setTimeout(resolve, ms));
};

exports.simulateWorkCallback = function simulateWork(cb, ms = 2) {
setTimeout(cb, ms);
};
Loading

0 comments on commit 4782f33

Please sign in to comment.