Skip to content

Commit

Permalink
Merge pull request #1 from meteor/testing
Browse files Browse the repository at this point in the history
test: testing github workflow
  • Loading branch information
leonardoventurini authored Oct 16, 2024
2 parents c186cc6 + d79698b commit b0da9da
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 30 deletions.
45 changes: 45 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Test Meteor Perf Package

on:
push:
branches:
- main
pull_request:

jobs:
test:
runs-on: ubuntu-latest

env:
METEOR_VERSION: 3.0.4

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: 20

- name: Cache Meteor Installation
id: meteor-cache
uses: actions/cache@v3
with:
path: ~/.meteor
key: ${{ runner.os }}-meteor-${{ env.METEOR_VERSION }}
restore-keys: |
${{ runner.os }}-meteor-
- name: Install Meteor (if not cached)
if: steps.meteor-cache.outputs.cache-hit != 'true'
run: |
npx meteor@${{ env.METEOR_VERSION }}
- name: Export Path
run: |
echo "$HOME/.meteor" >> $GITHUB_PATH
- name: Run Tests
run: |
TEST_CLIENT=0 meteor test-packages ./ --driver-package meteortesting:mocha --no-release-check --once
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.vscode
node_modules
node_modules
.npm
42 changes: 31 additions & 11 deletions async-interceptor.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,45 @@
import async_hooks from 'node:async_hooks';

export const asyncResources = new Map();
// 50% of map limit
export const GC_LIMIT = Meteor.isPackageTest ? 7 : Math.pow(2, 23);

export const AsyncResourceMap = new Map();

export const AsyncInterceptor = async_hooks.createHook({
init(asyncId, type, triggerAsyncId, resource) {
logResourceCreation(asyncId, type, triggerAsyncId, resource);
init(asyncId, type) {
captureResource(asyncId, type);
},
});

function logResourceCreation(asyncId, type, triggerAsyncId, resource) {
const stack = (new Error()).stack.split('\n').slice(2).filter(line => {
return !['AsyncHook.init', 'node:internal/async_hooks'].some(fn => line.includes(fn));
}).join('\n');
function captureResource(asyncId, type) {
let stack = stackTrace();

stack = `${type}\n${stack}`;

if (AsyncResourceMap.size > GC_LIMIT) {
garbageCollectAsyncResources();
}

if (!asyncResources.has(stack)) {
asyncResources.set(stack, { count: 0, types: new Set() });
if (!AsyncResourceMap.has(stack)) {
AsyncResourceMap.set(stack, { count: 0, ts: Date.now() });
}

const resourceInfo = asyncResources.get(stack);
const resourceInfo = AsyncResourceMap.get(stack);
resourceInfo.count++;
resourceInfo.types.add(type);
}

function garbageCollectAsyncResources() {
AsyncResourceMap.forEach((info, stack) => {
if (info.count <= 1) {
AsyncResourceMap.delete(stack);
}
});
}

export function stackTrace () {
return (new Error()).stack.split('\n').slice(3).filter(line => {
return !['AsyncHook.init', 'node:internal/async_hooks'].some(fn => line.includes(fn));
}).join('\n')
}

setInterval(garbageCollectAsyncResources, 10000);
3 changes: 2 additions & 1 deletion package.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ Package.onTest(function(api) {
Npm.depends({
'chai': '4.3.4',
'sinon': '10.0.0',
'sinon-chai': '3.6.0'
'sinon-chai': '3.6.0',
'benchmarkify': '4.0.0'
});

api.mainModule('tests/index.js', 'server');
Expand Down
5 changes: 2 additions & 3 deletions save-output.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { asyncResources } from './async-interceptor';
import { AsyncResourceMap } from './async-interceptor';
import { StatDict } from './observer-monitor';
import path from 'node:path';
import fs from 'node:fs';
Expand All @@ -9,14 +9,13 @@ export function saveOutput() {
performance.mark('saveOutput-start');
let async_traces = []

asyncResources.forEach((info, stack) => {
AsyncResourceMap.forEach((info, stack) => {
if (info.count <= 1) {
return;
}

async_traces.push({
count: info.count,
types: [...info.types],
stack,
});
});
Expand Down
152 changes: 152 additions & 0 deletions tests/async-interceptor.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { expect } from 'chai';
import { AsyncInterceptor, AsyncResourceMap, GC_LIMIT, stackTrace } from '../async-interceptor';
import Benchmarkify from "benchmarkify";

const benchmark = new Benchmarkify("Meteor Perf").printHeader();

benchmark.createSuite("Stack Trace", { time: 1000 })
.add("default error stack trace", () => {
return new Error().stack;
})
.ref("clean stack trace", () => {
return (new Error()).stack.split('\n').slice(2).filter(line => {
return !['AsyncHook.init', 'node:internal/async_hooks'].some(fn => line.includes(fn));
}).join('\n');
});

benchmark.createSuite("Interceptor Overhead", { time: 3000 })
.setup(() => {
AsyncInterceptor.enable();
})
.tearDown(() => {
AsyncInterceptor.disable();
})
.add("on", async (done) => {
await new Promise((resolve) => {
setTimeout(resolve, 0);
})

done()
})
.ref("off", async (done) => {
AsyncInterceptor.disable();

await new Promise((resolve) => {
setTimeout(resolve, 0);
})

done()
});



await benchmark.run();

describe('Async Interceptor', () => {
beforeEach(() => {
AsyncResourceMap.clear();
})

it('get stack trace', async () => {
const stack = stackTrace();

expect(stack).to.be.a('string');
});

it('should capture async operations', async () => {
AsyncInterceptor.enable();

await new Promise((resolve) => {
setTimeout(resolve, 100);
});

AsyncInterceptor.disable();

expect(AsyncResourceMap.size).to.be.greaterThan(0);
})

it('should not capture async operations when disabled', async () => {
AsyncInterceptor.disable();

AsyncResourceMap.clear();

await new Promise((resolve) => {
setTimeout(resolve, 100);
});

expect(AsyncResourceMap.size).to.equal(0);
});

it('should garbage collect async resources', async () => {
AsyncInterceptor.enable();

for (let i = 0; i < GC_LIMIT + 1; i++) {
await new Promise((resolve) => {
setTimeout(resolve, 1);
});
await new Promise((resolve) => {
setTimeout(resolve, 1);
});
await new Promise((resolve) => {
setTimeout(resolve, 1);
});
await new Promise((resolve) => {
setTimeout(resolve, 1);
});
await new Promise((resolve) => {
setTimeout(resolve, 1);
});
await new Promise((resolve) => {
setTimeout(resolve, 1);
});
await new Promise((resolve) => {
setTimeout(resolve, 1);
});
await new Promise((resolve) => {
setTimeout(resolve, 1);
});
await new Promise((resolve) => {
setTimeout(resolve, 1);
});
await new Promise((resolve) => {
setTimeout(resolve, 1);
});
await new Promise((resolve) => {
setTimeout(resolve, 1);
});
await new Promise((resolve) => {
setTimeout(resolve, 1);
});
await new Promise((resolve) => {
setTimeout(resolve, 1);
});
await new Promise((resolve) => {
setTimeout(resolve, 1);
});
await new Promise((resolve) => {
setTimeout(resolve, 1);
});
await new Promise((resolve) => {
setTimeout(resolve, 1);
});
}

AsyncInterceptor.disable();

expect(AsyncResourceMap.size).to.be.lessThanOrEqual(GC_LIMIT);
})

it('should have ts timestamp attribute for all trace entries', async () => {
AsyncInterceptor.enable();

await new Promise((resolve) => {
setTimeout(resolve, 100);
});

AsyncInterceptor.disable();

AsyncResourceMap.forEach((info) => {
expect(info).to.have.property('ts');
});
});
});
3 changes: 2 additions & 1 deletion tests/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
import './observer.test';
import './observer.test';
import './async-interceptor.test';
15 changes: 2 additions & 13 deletions tests/observer.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { StatDict } from '../observer-monitor';
import { MeteorPerf } from '../index';
import { expect } from 'chai';

const coll = new Mongo.Collection('test')
Expand All @@ -11,10 +10,7 @@ describe('Observer', () => {
await coll.removeAsync({}, { multi: true })

observer = await coll.find({}).observeChanges({
added: (...args) => {
console.trace()
console.log('added', args);
},
added: (...args) => console.log('added', args),
changed: (...args) => console.log('changed', args),
removed: (...args) => console.log('removed', args),
})
Expand All @@ -26,14 +22,7 @@ describe('Observer', () => {
});

it('should detect observer creation', async () => {
console.log(await coll.insertAsync({ foo: 'bar' }))
console.log(await coll.insertAsync({ foo: 'bar' }))
console.log(await coll.insertAsync({ foo: 'bar' }))
console.log(await coll.insertAsync({ foo: 'bar' }))

Meteor._sleepForMs(5000)

console.log({ StatDict, MeteorPerf})
await coll.insertAsync({ foo: 'bar' })

expect(StatDict.size).to.equal(1);
expect(StatDict.get('test::{}')).to.have.property('key', 'test::{}');
Expand Down

0 comments on commit b0da9da

Please sign in to comment.