Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance counter property tests #1990

Merged
merged 7 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ jobs:
"tests/property/ic_api/id",
"tests/property/ic_api/instruction_counter",
"tests/property/ic_api/is_controller",
"tests/property/ic_api/performance_counter",
"tests/property/ic_api/time",
"tests/property/ic_api/trap"
]
Expand Down
4 changes: 4 additions & 0 deletions tests/property/ic_api/performance_counter/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.azle
.dfx
dfx_generated
node_modules
13 changes: 13 additions & 0 deletions tests/property/ic_api/performance_counter/dfx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"canisters": {
"canister": {
"type": "azle",
"main": "src/index.ts",
"candid_gen": "automatic",
"declarations": {
"output": "test/dfx_generated/canister",
"node_compatibility": true
}
}
}
}
12 changes: 12 additions & 0 deletions tests/property/ic_api/performance_counter/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
bail: true,
testTimeout: 100_000_000,
transform: {
'^.+\\.ts$': ['ts-jest', { isolatedModules: true }],
'^.+\\.js$': 'ts-jest'
},
transformIgnorePatterns: ['/node_modules/(?!(azle)/)'] // Make sure azle is transformed
};
6,231 changes: 6,231 additions & 0 deletions tests/property/ic_api/performance_counter/package-lock.json

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions tests/property/ic_api/performance_counter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"scripts": {
"pretest": "tsx test/pretest.ts",
"test": "jest"
},
"dependencies": {
"azle": "^0.23.0"
},
"devDependencies": {
"@dfinity/agent": "^2.0.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.4",
"tsx": "^4.15.7",
"typescript": "^5.2.2"
}
}
43 changes: 43 additions & 0 deletions tests/property/ic_api/performance_counter/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { chunk, IDL, performanceCounter, query, update } from 'azle';

export default class {
@query([IDL.Nat32], IDL.Nat64)
async queryPerformanceCounter0(loops: number): Promise<bigint> {
await sum(loops, false);

return performanceCounter(0);
}

@update([IDL.Nat32], IDL.Nat64)
async updatePerformanceCounter0(loops: number): Promise<bigint> {
await sum(loops, false);

return performanceCounter(0);
}

@query([IDL.Nat32], IDL.Nat64)
async queryPerformanceCounter1(loops: number): Promise<bigint> {
await sum(loops, false);

return performanceCounter(1);
}

@update([IDL.Nat32], IDL.Nat64)
async updatePerformanceCounter1(loops: number): Promise<bigint> {
await sum(loops, true);

return performanceCounter(1);
}
}

async function sum(loops: number, shouldChunk: boolean): Promise<void> {
let _sum = 0;

for (let i = 0; i < loops; i++) {
_sum += (i % 100) * (i % 100);

if (shouldChunk && i % 100_000 === 0) {
await chunk();
}
}
}
17 changes: 17 additions & 0 deletions tests/property/ic_api/performance_counter/test/pretest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { execSync } from 'child_process';

function pretest(): void {
execSync(`dfx canister uninstall-code canister || true`, {
stdio: 'inherit'
});

execSync(`dfx deploy canister`, {
stdio: 'inherit'
});

execSync(`dfx generate canister`, {
stdio: 'inherit'
});
}

pretest();
5 changes: 5 additions & 0 deletions tests/property/ic_api/performance_counter/test/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { runTests } from 'azle/test';

import { getTests } from './tests';

runTests(getTests());
231 changes: 231 additions & 0 deletions tests/property/ic_api/performance_counter/test/tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import { defaultParams, expect, getCanisterActor, it, Test } from 'azle/test';
import fc from 'fast-check';

import { _SERVICE as Actor } from './dfx_generated/canister/canister.did';

export function getTests(): Test {
return () => {
it('should calculate performanceCounter(0) instructions accurately from a query method', async () => {
const actor = await getCanisterActor<Actor>('canister');

await fc.assert(
fc.asyncProperty(
fc.nat({
max: 100 // Our algorithm for determinstically checking the number of instructions based on the number of loops breaks down soon after 100 iterations
bdemann marked this conversation as resolved.
Show resolved Hide resolved
}),
async (loops) => {
const instructions =
await actor.queryPerformanceCounter0(loops);

const {
loops0Instructions,
zeroToOneDelta,
oneToTwoDelta
} = await getBaseInstructionCountsQuery0();

expect(instructions).toStrictEqual(
bdemann marked this conversation as resolved.
Show resolved Hide resolved
loops0Instructions +
(loops === 0
? 0n
: loops === 1
? zeroToOneDelta
: zeroToOneDelta +
(BigInt(loops) - 1n) * oneToTwoDelta)
);
}
),
defaultParams
);
});

// This test is much different than the query test because for some reason
// the number of instructions per call and even per call with the same number of loops
// is not consistent in a few ways. For example calling loops 0 repeatedly returns
// a different number of instructions in the first few calls
it('should calculate performanceCounter(0) instructions accurately from an update method', async () => {
const actor = await getCanisterActor<Actor>('canister');

await fc.assert(
fc.asyncProperty(
fc.nat({
max: 1_000_000
}),
async (loops) => {
const instructionsLoops0 =
await actor.updatePerformanceCounter0(0);

const instructions0 =
await actor.updatePerformanceCounter0(loops);
const instructions1 =
await actor.updatePerformanceCounter0(loops);

const instructionsAfter0 =
await actor.updatePerformanceCounter0(loops + 1);
const instructionsAfter1 =
await actor.updatePerformanceCounter0(loops + 1);

expect(instructionsLoops0).not.toStrictEqual(0n);

expect(
bigIntAbs(instructions0 - instructions1)
).toBeLessThan(1_000n);

expect(
bigIntAbs(instructionsAfter0 - instructionsAfter1)
).toBeLessThan(1_000n);

expect(instructions0).toBeLessThan(instructionsAfter0);
expect(instructions1).toBeLessThan(instructionsAfter1);
}
),
defaultParams
);
});

it('should calculate performanceCounter(1) instructions accurately from a query method', async () => {
const actor = await getCanisterActor<Actor>('canister');

await fc.assert(
fc.asyncProperty(
fc.nat({
max: 100 // Our algorithm for determinstically checking the number of instructions based on the number of loops breaks down soon after 100 iterations
}),
async (loops) => {
const instructions =
await actor.queryPerformanceCounter1(loops);

const {
loops0Instructions,
zeroToOneDelta,
oneToTwoDelta
} = await getBaseInstructionCountsQuery1();

expect(instructions).toStrictEqual(
loops0Instructions +
(loops === 0
? 0n
: loops === 1
? zeroToOneDelta
: zeroToOneDelta +
(BigInt(loops) - 1n) * oneToTwoDelta)
);
}
),
defaultParams
);
});

// This test is much different than the query test because for some reason
// the number of instructions per call and even per call with the same number of loops
// is not consistent in a few ways. For example calling loops 0 repeatedly returns
// a different number of instructions in the first few calls
it('should calculate performanceCounter(1) instructions accurately from an update method', async () => {
const actor = await getCanisterActor<Actor>('canister');

await fc.assert(
fc.asyncProperty(
fc.nat({
max: 1_000_000
}),
async (loops) => {
const instructionsLoops0 =
await actor.updatePerformanceCounter1(0);

const instructions0 =
await actor.updatePerformanceCounter1(loops);
const instructions1 =
await actor.updatePerformanceCounter1(loops);

const instructionsAfter0 =
await actor.updatePerformanceCounter1(loops + 100);
const instructionsAfter1 =
await actor.updatePerformanceCounter1(loops + 100);

expect(instructionsLoops0).not.toStrictEqual(0n);

expect(
percentageDifferenceBigInt(
instructions0,
instructions1
)
).toBeLessThanOrEqual(5n);

expect(
percentageDifferenceBigInt(
instructionsAfter0,
instructionsAfter1
)
).toBeLessThanOrEqual(5n);

expect(instructions0).toBeLessThan(instructionsAfter0);
expect(instructions1).toBeLessThan(instructionsAfter1);
}
),
defaultParams
);
});
};
}

type BaseInstructionCounts = {
loops0Instructions: bigint;
loops1Instructions: bigint;
loops2Instructions: bigint;
zeroToOneDelta: bigint;
oneToTwoDelta: bigint;
};

async function getBaseInstructionCountsQuery0(): Promise<BaseInstructionCounts> {
const actor = await getCanisterActor<Actor>('canister');

await actor.queryPerformanceCounter0(0);
await actor.queryPerformanceCounter0(0);

const loops0Instructions = await actor.queryPerformanceCounter0(0);
const loops1Instructions = await actor.queryPerformanceCounter0(1);
const loops2Instructions = await actor.queryPerformanceCounter0(2);

const zeroToOneDelta = loops1Instructions - loops0Instructions;
const oneToTwoDelta = loops2Instructions - loops1Instructions;

return {
loops0Instructions,
loops1Instructions,
loops2Instructions,
zeroToOneDelta,
oneToTwoDelta
};
}

async function getBaseInstructionCountsQuery1(): Promise<BaseInstructionCounts> {
const actor = await getCanisterActor<Actor>('canister');

await actor.queryPerformanceCounter1(0);
await actor.queryPerformanceCounter1(0);

const loops0Instructions = await actor.queryPerformanceCounter1(0);
const loops1Instructions = await actor.queryPerformanceCounter1(1);
const loops2Instructions = await actor.queryPerformanceCounter1(2);

const zeroToOneDelta = loops1Instructions - loops0Instructions;
const oneToTwoDelta = loops2Instructions - loops1Instructions;

return {
loops0Instructions,
loops1Instructions,
loops2Instructions,
zeroToOneDelta,
oneToTwoDelta
};
}

function bigIntAbs(x: bigint): bigint {
return x < 0 ? -x : x;
}

function percentageDifferenceBigInt(value1: bigint, value2: bigint): bigint {
const difference = value1 > value2 ? value1 - value2 : value2 - value1;
const average = (value1 + value2) / 2n;
const percentageDifference = (difference * 100n) / average;
return percentageDifference;
}
10 changes: 10 additions & 0 deletions tests/property/ic_api/performance_counter/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"strict": true,
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "node",
"allowJs": true,
"outDir": "HACK_BECAUSE_OF_ALLOW_JS"
}
}
Loading