mirror of
https://github.com/4ian/GDevelop.git
synced 2026-10-12 02:01:17 +00:00
Add micro and macro benchmarks on CI (#9206)
Only show in developer changelog
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# GitHub Action to run the benchmarks of the game engine (GDJS/tests/benchmarks)
|
||||
# on the changes made by a pull request, compared to its base branch.
|
||||
# On master, each change is compared to the previous commit, so the results
|
||||
# (in the summary of the runs, and as artifacts) track the evolution over time.
|
||||
|
||||
name: Benchmarks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "GDJS/**"
|
||||
- "Extensions/**"
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "GDJS/**"
|
||||
- "Extensions/**"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: benchmarks-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
compare-benchmarks:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
# To comment the results on the pull request.
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# The base version is built from the history.
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
GDJS/package-lock.json
|
||||
GDJS/tests/package-lock.json
|
||||
|
||||
- name: Install GDJS dependencies
|
||||
run: npm ci
|
||||
working-directory: GDJS
|
||||
|
||||
- name: Install GDJS tests dependencies
|
||||
run: npm ci
|
||||
working-directory: GDJS/tests
|
||||
|
||||
- name: Compare the benchmarks with the base version
|
||||
run: npm run benchmark:compare -- --base-ref="$BASE_REF" --markdown=benchmark-results.md --json=benchmark-results.json
|
||||
working-directory: GDJS/tests
|
||||
env:
|
||||
BASE_REF: ${{ github.event_name == 'pull_request' && format('origin/{0}', github.base_ref) || 'HEAD^' }}
|
||||
|
||||
- name: Show the results in the summary
|
||||
run: cat benchmark-results.md >> "$GITHUB_STEP_SUMMARY"
|
||||
working-directory: GDJS/tests
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: benchmark-results-${{ github.sha }}
|
||||
path: GDJS/tests/benchmark-results.json
|
||||
retention-days: 90
|
||||
|
||||
# Pull requests from forks have no permission to comment: their results
|
||||
# are in the summary of the run.
|
||||
- name: Comment the results on the pull request
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
# Don't fail the build if commenting is not possible.
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
|
||||
working-directory: GDJS/tests
|
||||
# Update the previous comment (found with the marker starting the
|
||||
# report) rather than adding a new one at each push.
|
||||
run: |
|
||||
COMMENT_ID=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PULL_REQUEST_NUMBER/comments" --paginate --jq '.[] | select(.body | startswith("<!-- benchmark-results -->")) | .id' | head -n 1)
|
||||
if [ -n "$COMMENT_ID" ]; then
|
||||
gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$COMMENT_ID" -F [email protected]
|
||||
else
|
||||
gh pr comment "$PULL_REQUEST_NUMBER" --body-file benchmark-results.md
|
||||
fi
|
||||
@@ -34,3 +34,4 @@
|
||||
.Trashes
|
||||
Thumbs.db
|
||||
.claude
|
||||
/GDJS/tests/.benchmark-runtimes
|
||||
|
||||
+30
-23
@@ -2,7 +2,7 @@ describe('gdjs.PlatformerObjectRuntimeBehavior Benchmark', function () {
|
||||
let runtimeScene;
|
||||
let objects;
|
||||
const duplicateCount = 60;
|
||||
const stepCount = 6000;
|
||||
const stepCount = 1000;
|
||||
|
||||
beforeEach(function () {
|
||||
runtimeScene = makePlatformerTestRuntimeScene();
|
||||
@@ -30,6 +30,8 @@ describe('gdjs.PlatformerObjectRuntimeBehavior Benchmark', function () {
|
||||
roundCoordinates: true,
|
||||
},
|
||||
],
|
||||
variables: [],
|
||||
effects: [],
|
||||
});
|
||||
object.getWidth = function () {
|
||||
return 10;
|
||||
@@ -50,29 +52,34 @@ describe('gdjs.PlatformerObjectRuntimeBehavior Benchmark', function () {
|
||||
});
|
||||
|
||||
it('benchmark', function () {
|
||||
this.timeout(30000);
|
||||
this.timeout(60000);
|
||||
|
||||
for (let b = 0; b < 10; ++b) {
|
||||
const benchmarkSuite = makeBenchmarkSuite({
|
||||
benchmarksCount: 1,
|
||||
iterationsCount: stepCount,
|
||||
});
|
||||
benchmarkSuite.add('jump in loop', (t) => {
|
||||
for (let i = 0; i < duplicateCount; ++i) {
|
||||
const object = objects[i];
|
||||
if (t % 60 == i % 60) {
|
||||
object.getBehavior('auto1').simulateJumpKey();
|
||||
}
|
||||
if (t + (i % 61) < 31) {
|
||||
object.getBehavior('auto1').simulateRightKey();
|
||||
}
|
||||
if (t + (i % 61) >= 31) {
|
||||
object.getBehavior('auto1').simulateLeftKey();
|
||||
}
|
||||
// Many short batches: the median is less sensitive to a slow batch
|
||||
// (garbage collection...).
|
||||
const benchmarkSuite = makeBenchmarkSuite({
|
||||
benchmarksCount: 60,
|
||||
iterationsCount: stepCount,
|
||||
});
|
||||
// Counted across batches: the objects move the same way whatever the
|
||||
// batch size.
|
||||
let frameIndex = 0;
|
||||
benchmarkSuite.add('platformer objects jumping in loop', () => {
|
||||
const t = frameIndex++;
|
||||
for (let i = 0; i < duplicateCount; ++i) {
|
||||
const object = objects[i];
|
||||
if (t % 60 == i % 60) {
|
||||
object.getBehavior('auto1').simulateJumpKey();
|
||||
}
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
});
|
||||
console.log(benchmarkSuite.run());
|
||||
}
|
||||
// Go right then left, to stay on the platforms: the same work is
|
||||
// measured by every batch.
|
||||
if ((t + i) % 60 < 30) {
|
||||
object.getBehavior('auto1').simulateRightKey();
|
||||
} else {
|
||||
object.getBehavior('auto1').simulateLeftKey();
|
||||
}
|
||||
}
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
});
|
||||
console.log(benchmarkSuite.run());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -361,6 +361,29 @@ namespace gdjs {
|
||||
jsHeapUsedMb?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* The measures of `harness.benchmark`: the time of each measured frame
|
||||
* (game logic and rendering), and counters that don't depend on the
|
||||
* machine, to compare two versions of the engine running the same
|
||||
* scenario.
|
||||
*/
|
||||
export type GameplayTestBenchmarkResult = {
|
||||
name: string;
|
||||
/** The time of each measured frame, in order. */
|
||||
frameTimesMs: Array<number>;
|
||||
medianFrameTimeMs: number;
|
||||
/** The profiled sections (see `GameplayTestProfilingResult`), for
|
||||
* the last 600 measured frames at most. */
|
||||
sections: Array<{ name: string; avgTimeMs: number; maxTimeMs: number }>;
|
||||
/** WebGL draw calls (2D and 3D) per frame, or null without renderer. */
|
||||
drawCallsPerFrame: number | null;
|
||||
objectsCount: integer;
|
||||
/** A hash of the position and angle of every object at the end: two
|
||||
* versions of the engine running the same scenario must end on the
|
||||
* same state. */
|
||||
stateChecksum: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The outcome of `lookTowardWithMouseDelta`: whether the aim succeeded,
|
||||
* the remaining aim error, and whether the game responded to the mouse
|
||||
@@ -442,6 +465,8 @@ namespace gdjs {
|
||||
screenshotsTakenCount: integer;
|
||||
/** The `stopProfiling()` summaries captured during the run. */
|
||||
profiles: Array<GameplayTestProfilingResult>;
|
||||
/** The `benchmark()` measures captured during the run. */
|
||||
benchmarks: Array<GameplayTestBenchmarkResult>;
|
||||
performance: {
|
||||
avgStepMs: number;
|
||||
worstStepMs: number;
|
||||
@@ -476,6 +501,9 @@ namespace gdjs {
|
||||
const MAX_PROFILING_TIMELINE_ENTRIES = 120;
|
||||
const MAX_PROFILING_WORST_FRAMES = 5;
|
||||
const MAX_PROFILES_PER_RESULT = 5;
|
||||
const MAX_BENCHMARKS_PER_RESULT = 20;
|
||||
const DEFAULT_BENCHMARK_WARMUP_FRAMES = 60;
|
||||
const DEFAULT_BENCHMARK_FRAMES = 300;
|
||||
/** The scene change cause declared by the harness (see
|
||||
* `SceneStack.runWithSceneChangeCause`). */
|
||||
const GAMEPLAY_TEST_SCENE_CHANGE_CAUSE = 'gameplayTest';
|
||||
@@ -757,6 +785,10 @@ namespace gdjs {
|
||||
/** The profiling summaries captured during the run (attached to the
|
||||
* result as `profiles`). */
|
||||
_profiles: Array<GameplayTestProfilingResult> = [];
|
||||
_benchmarks: Array<GameplayTestBenchmarkResult> = [];
|
||||
/** The `Math.random` replaced by `setRandomSeed`, restored at the end
|
||||
* of the run. */
|
||||
_originalMathRandom: (() => number) | null = null;
|
||||
_timeoutMs: number;
|
||||
_loadingTimeoutMs: number;
|
||||
/** Time spent waiting for loading (game boot, scene assets) so far:
|
||||
@@ -1290,17 +1322,17 @@ namespace gdjs {
|
||||
|
||||
/**
|
||||
* Step a single game frame (game logic + rendering) with a fixed
|
||||
* time delta.
|
||||
* time delta, and return the time it took.
|
||||
*/
|
||||
_stepSingleFrame(dtMs: float): void {
|
||||
_stepSingleFrame(dtMs: float): float {
|
||||
this._checkGuards();
|
||||
const stepStartTimeMs = Date.now();
|
||||
const stepStartTimeMs = performance.now();
|
||||
this._runtimeGame.getSceneStack().step(dtMs);
|
||||
const stepTimeMs = performance.now() - stepStartTimeMs;
|
||||
this._callOnFrameEnded();
|
||||
this._framesExecuted++;
|
||||
this._gameTimeMs += dtMs;
|
||||
const stepTimeMs = Date.now() - stepStartTimeMs;
|
||||
this._lastFrameStepTimeMs = stepStartTimeMs + stepTimeMs;
|
||||
this._lastFrameStepTimeMs = Date.now();
|
||||
this._totalStepTimeMs += stepTimeMs;
|
||||
if (stepTimeMs > this._worstStepTimeMs) {
|
||||
this._worstStepTimeMs = stepTimeMs;
|
||||
@@ -1311,6 +1343,7 @@ namespace gdjs {
|
||||
this._lastProgressTimeMs = Date.now();
|
||||
this._onProgress(this._framesExecuted);
|
||||
}
|
||||
return stepTimeMs;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3622,6 +3655,162 @@ namespace gdjs {
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace `Math.random` by a generator seeded with the given number
|
||||
* until the end of the test, so that the game makes the same random
|
||||
* choices at every run.
|
||||
*/
|
||||
setRandomSeed(seed: integer): void {
|
||||
if (!this._originalMathRandom) {
|
||||
this._originalMathRandom = Math.random;
|
||||
}
|
||||
// Mulberry32, a small and fast pseudo-random generator.
|
||||
let state = seed >>> 0;
|
||||
Math.random = () => {
|
||||
state = (state + 0x6d2b79f5) >>> 0;
|
||||
let t = state;
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
_restoreMathRandom(): void {
|
||||
if (this._originalMathRandom) {
|
||||
Math.random = this._originalMathRandom;
|
||||
this._originalMathRandom = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step `warmupFrames` frames (not measured: the JavaScript engine
|
||||
* optimizes the code meanwhile), then measure each of the next
|
||||
* `frames` frames, the profiled sections, the draw calls and a
|
||||
* checksum of the final state.
|
||||
* Frames never wait for an animation frame (which renders the game
|
||||
* once more, and can be delayed by the browser), so that two runs of
|
||||
* the same scenario do the same work.
|
||||
*/
|
||||
async benchmark(
|
||||
name: string,
|
||||
options?: { warmupFrames?: integer; frames?: integer; dtMs?: float }
|
||||
): Promise<GameplayTestBenchmarkResult> {
|
||||
const warmupFrames =
|
||||
options && options.warmupFrames !== undefined
|
||||
? options.warmupFrames
|
||||
: DEFAULT_BENCHMARK_WARMUP_FRAMES;
|
||||
const frames = (options && options.frames) || DEFAULT_BENCHMARK_FRAMES;
|
||||
const dtMs = (options && options.dtMs) || DEFAULT_FRAME_DT_MS;
|
||||
const frameTimesMs: Array<number> = [];
|
||||
const stepFramesWithoutRendering = async (
|
||||
frameCount: integer,
|
||||
isMeasured: boolean
|
||||
) => {
|
||||
for (let i = 0; i < frameCount; i++) {
|
||||
const stepTimeMs = this._stepSingleFrame(dtMs);
|
||||
if (isMeasured) {
|
||||
frameTimesMs.push(Math.round(stepTimeMs * 1000) / 1000);
|
||||
}
|
||||
await this._waitForGameSceneSwitchIfNeeded();
|
||||
if (Date.now() - this._lastYieldTimeMs >= YIELD_BUDGET_MS) {
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
||||
this._lastYieldTimeMs = Date.now();
|
||||
}
|
||||
}
|
||||
};
|
||||
await stepFramesWithoutRendering(warmupFrames, false);
|
||||
|
||||
// Available when the browser is launched with `--js-flags=--expose-gc`.
|
||||
const collectGarbage = (globalThis as any).gc;
|
||||
if (typeof collectGarbage === 'function') collectGarbage();
|
||||
|
||||
const drawCallsCounter = this._countDrawCalls();
|
||||
this.startProfiling();
|
||||
try {
|
||||
await stepFramesWithoutRendering(frames, true);
|
||||
} finally {
|
||||
if (drawCallsCounter) drawCallsCounter.uninstall();
|
||||
}
|
||||
const profile = this.stopProfiling();
|
||||
|
||||
const sortedFrameTimesMs = [...frameTimesMs].sort((a, b) => a - b);
|
||||
const benchmarkResult: GameplayTestBenchmarkResult = {
|
||||
name,
|
||||
frameTimesMs,
|
||||
medianFrameTimeMs:
|
||||
sortedFrameTimesMs[Math.floor(sortedFrameTimesMs.length / 2)] || 0,
|
||||
sections: profile ? profile.sections : [],
|
||||
drawCallsPerFrame: drawCallsCounter
|
||||
? Math.round((drawCallsCounter.getCount() / frames) * 100) / 100
|
||||
: null,
|
||||
objectsCount:
|
||||
this._getCurrentScene().getAdhocListOfAllInstances().length,
|
||||
stateChecksum: this._computeStateChecksum(),
|
||||
};
|
||||
this._benchmarks.push(benchmarkResult);
|
||||
if (this._benchmarks.length > MAX_BENCHMARKS_PER_RESULT) {
|
||||
this._benchmarks.shift();
|
||||
}
|
||||
return benchmarkResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the WebGL draw calls, made by PixiJS and Three.js (which
|
||||
* share the same WebGL context), until `uninstall` is called.
|
||||
*/
|
||||
private _countDrawCalls(): {
|
||||
getCount: () => integer;
|
||||
uninstall: () => void;
|
||||
} | null {
|
||||
const pixiRenderer = this._runtimeGame.getRenderer().getPIXIRenderer();
|
||||
const gl: any = pixiRenderer ? pixiRenderer.gl : null;
|
||||
if (!gl) return null;
|
||||
let count = 0;
|
||||
const originalMethods: { [methodName: string]: Function } = {};
|
||||
for (const methodName of [
|
||||
'drawArrays',
|
||||
'drawElements',
|
||||
'drawArraysInstanced',
|
||||
'drawElementsInstanced',
|
||||
]) {
|
||||
const originalMethod = gl[methodName];
|
||||
if (!originalMethod) continue;
|
||||
originalMethods[methodName] = originalMethod;
|
||||
gl[methodName] = function () {
|
||||
count++;
|
||||
return originalMethod.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
return {
|
||||
getCount: () => count,
|
||||
uninstall: () => {
|
||||
for (const methodName in originalMethods) {
|
||||
gl[methodName] = originalMethods[methodName];
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _computeStateChecksum(): string {
|
||||
const instances = [
|
||||
...this._getCurrentScene().getAdhocListOfAllInstances(),
|
||||
].sort((a, b) => a.id - b.id);
|
||||
// FNV-1a hash of the rounded values.
|
||||
let hash = 0x811c9dc5;
|
||||
const addText = (text: string) => {
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
hash = Math.imul(hash ^ text.charCodeAt(i), 16777619);
|
||||
}
|
||||
};
|
||||
for (const instance of instances) {
|
||||
addText(instance.getName());
|
||||
addText(instance.getX().toFixed(2));
|
||||
addText(instance.getY().toFixed(2));
|
||||
addText(instance.getAngle().toFixed(2));
|
||||
}
|
||||
return (hash >>> 0).toString(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a console log in the test result (also shown in the
|
||||
* browser console). Used by the `console` given to the script.
|
||||
@@ -3772,6 +3961,7 @@ namespace gdjs {
|
||||
screenshots: this._screenshots,
|
||||
screenshotsTakenCount: this._screenshotsTakenCount,
|
||||
profiles: this._profiles,
|
||||
benchmarks: this._benchmarks,
|
||||
performance:
|
||||
this._framesExecuted > 0
|
||||
? {
|
||||
@@ -3779,7 +3969,7 @@ namespace gdjs {
|
||||
Math.round(
|
||||
(this._totalStepTimeMs / this._framesExecuted) * 100
|
||||
) / 100,
|
||||
worstStepMs: this._worstStepTimeMs,
|
||||
worstStepMs: Math.round(this._worstStepTimeMs * 100) / 100,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
@@ -4179,6 +4369,7 @@ namespace gdjs {
|
||||
// Ignore errors during cleanup.
|
||||
}
|
||||
inputManager.onFrameEnded = originalOnFrameEnded;
|
||||
harness._restoreMathRandom();
|
||||
harness._uninstallPageVisibilityTracking();
|
||||
harness._uninstallPointerLockShim();
|
||||
harness._uninstallSoundLog();
|
||||
|
||||
@@ -27,6 +27,18 @@ Tests are launched using Chrome. You need Chrome installed to run them. You can
|
||||
|
||||
Tests are located in the **tests** folder for the game engine, or directly in the folder of the tested extensions.
|
||||
|
||||
### Benchmarks
|
||||
|
||||
Benchmarks are in the **benchmarks** folder: micro benchmarks of the game engine functions, and scene benchmarks (in **benchmarks/scenes**) running scenes like a game with the gameplay test harness (`harness.benchmark`).
|
||||
|
||||
To compare the speed of your changes with the version your branch started from:
|
||||
|
||||
```bash
|
||||
npm run benchmark:compare -- --base-ref=origin/master --grep="Scene benchmarks" # --grep is optional
|
||||
```
|
||||
|
||||
Both versions are built and the benchmarks are run alternately on each one (3 times by default, set with `--rounds`). A change is only reported when larger than 5% and measured between every round of one version and every round of the other, as speed varies between runs of a browser (by up to ~15% on a shared machine). The same comparison runs on pull requests changing the game engine (see `.github/workflows/benchmarks.yml`).
|
||||
|
||||
### Games in the _games_ folder
|
||||
|
||||
Games contained in the _games_ folder are mainly here to be launched manually to check that a particular feature is working. Read the comments in the events to see what is the expected behavior, or compare with the native platform if you can.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// @ts-check
|
||||
|
||||
describe('gdjs.evtTools.object.hitBoxesCollisionTest', function () {
|
||||
const runtimeGame = gdjs.getPixiRuntimeGame();
|
||||
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
|
||||
|
||||
/**
|
||||
* Objects of 32x32 pixels spread on an area growing with their number, so
|
||||
* that each object overlaps about the same number of others.
|
||||
* @param {string} name
|
||||
* @param {number} count
|
||||
* @param {() => number} random
|
||||
*/
|
||||
const createObjects = (name, count, random) => {
|
||||
const areaSize = Math.sqrt(count) * 64;
|
||||
const objects = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const object = new gdjs.TestRuntimeObject(runtimeScene, {
|
||||
name,
|
||||
type: '',
|
||||
behaviors: [],
|
||||
effects: [],
|
||||
variables: [],
|
||||
});
|
||||
object.setCustomWidthAndHeight(32, 32);
|
||||
object.setPosition(random() * areaSize, random() * areaSize);
|
||||
objects.push(object);
|
||||
}
|
||||
return objects;
|
||||
};
|
||||
|
||||
for (const objectsCount of [10, 100, 500, 1000]) {
|
||||
it(`benchmark ${objectsCount} objects colliding with ${objectsCount} other objects`, function () {
|
||||
this.timeout(60000);
|
||||
const random = makeSeededRandom(objectsCount);
|
||||
const allEnemies = createObjects('Enemy', objectsCount, random);
|
||||
const allBullets = createObjects('Bullet', objectsCount, random);
|
||||
|
||||
// Like the code generated for events, lists are refilled before each
|
||||
// condition, as the condition removes the objects not picked.
|
||||
const enemies = [];
|
||||
const bullets = [];
|
||||
const enemiesLists = Hashtable.newFrom({ Enemy: enemies });
|
||||
const bulletsLists = Hashtable.newFrom({ Bullet: bullets });
|
||||
|
||||
const benchmarkSuite = makeBenchmarkSuite({
|
||||
benchmarksCount: 30,
|
||||
// About the same duration for each objects count, with a brute-force
|
||||
// test of every pair of objects.
|
||||
iterationsCount: Math.ceil(1000000 / (objectsCount * objectsCount)),
|
||||
});
|
||||
benchmarkSuite.add(
|
||||
`hitBoxesCollisionTest of ${objectsCount} objects with ${objectsCount} objects`,
|
||||
() => {
|
||||
gdjs.copyArray(allEnemies, enemies);
|
||||
gdjs.copyArray(allBullets, bullets);
|
||||
gdjs.evtTools.object.hitBoxesCollisionTest(
|
||||
enemiesLists,
|
||||
bulletsLists,
|
||||
false,
|
||||
runtimeScene,
|
||||
false
|
||||
);
|
||||
}
|
||||
);
|
||||
console.log(benchmarkSuite.run());
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,47 +1,104 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* name: string,
|
||||
* samplesMs: Array<number>,
|
||||
* counters?: {[counterName: string]: number | string | null},
|
||||
* }} BenchmarkResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* Send a benchmark result to the karma reporter writing the results file
|
||||
* read by `compare-benchmarks.js`. `samplesMs` are the measures to compare,
|
||||
* `counters` are values that must not depend on the machine (draw calls,
|
||||
* objects count...).
|
||||
* @param {BenchmarkResult} benchmarkResult
|
||||
*/
|
||||
const reportBenchmarkResult = (benchmarkResult) => {
|
||||
// @ts-ignore - Karma API to send information to the reporters.
|
||||
if (window.__karma__) window.__karma__.info({ benchmarkResult });
|
||||
};
|
||||
|
||||
/**
|
||||
* A pseudo-random generator, so that benchmarks always create the same
|
||||
* objects at the same positions.
|
||||
* @param {number} seed
|
||||
* @returns {() => number} A function returning a number in [0, 1[.
|
||||
*/
|
||||
const makeSeededRandom = (seed) => {
|
||||
// Mulberry32, a small and fast pseudo-random generator.
|
||||
let state = seed >>> 0;
|
||||
return () => {
|
||||
state = (state + 0x6d2b79f5) >>> 0;
|
||||
let t = state;
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper allowing to run a benchmark of the time spent the execute a certain
|
||||
* number of iterations of one or more functions.
|
||||
*
|
||||
* Note that this could surely be replaced by a more robust solution like
|
||||
* Benchmark.js
|
||||
* The first tenth of the batches are a warm-up (not measured), so that the
|
||||
* JavaScript engine optimizes the functions before they are measured.
|
||||
*
|
||||
* @param {*} options
|
||||
* @param {{benchmarksCount?: number, iterationsCount?: number}} options
|
||||
*/
|
||||
let makeBenchmarkSuite = (options = {}) => {
|
||||
const makeBenchmarkSuite = (options = {}) => {
|
||||
/** @type {{[benchmarkName: string]: Array<number>}} */
|
||||
const benchmarkTimings = {};
|
||||
const benchmarksCount = options.benchmarksCount || 1000;
|
||||
const benchmarksCount = options.benchmarksCount || 200;
|
||||
const iterationsCount = options.iterationsCount || 100000;
|
||||
const warmUpBenchmarksCount = Math.ceil(benchmarksCount / 10);
|
||||
/** @type {Array<{title: string, fn: (i: number) => void}>} */
|
||||
const testCases = [];
|
||||
|
||||
const suite = {};
|
||||
suite.add = (title, fn) => {
|
||||
testCases.push({ title, fn });
|
||||
return suite;
|
||||
};
|
||||
suite.run = () => {
|
||||
for (
|
||||
let benchmarkIndex = 0;
|
||||
benchmarkIndex < benchmarksCount;
|
||||
benchmarkIndex++
|
||||
) {
|
||||
testCases.forEach(testCase => {
|
||||
const description = testCase.title + '(' + iterationsCount + 'x)';
|
||||
const start = performance.now();
|
||||
for (let i = 0; i < iterationsCount; i++) {
|
||||
testCase.fn(i);
|
||||
}
|
||||
benchmarkTimings[description] = benchmarkTimings[description] || [];
|
||||
benchmarkTimings[description].push(performance.now() - start);
|
||||
});
|
||||
}
|
||||
const suite = {
|
||||
/**
|
||||
* @param {string} title
|
||||
* @param {(i: number) => void} fn
|
||||
*/
|
||||
add: (title, fn) => {
|
||||
testCases.push({ title, fn });
|
||||
return suite;
|
||||
},
|
||||
run: () => {
|
||||
for (
|
||||
let benchmarkIndex = -warmUpBenchmarksCount;
|
||||
benchmarkIndex < benchmarksCount;
|
||||
benchmarkIndex++
|
||||
) {
|
||||
testCases.forEach((testCase) => {
|
||||
const description = testCase.title + '(' + iterationsCount + 'x)';
|
||||
const start = performance.now();
|
||||
for (let i = 0; i < iterationsCount; i++) {
|
||||
testCase.fn(i);
|
||||
}
|
||||
const duration = performance.now() - start;
|
||||
if (benchmarkIndex < 0) return;
|
||||
benchmarkTimings[description] = benchmarkTimings[description] || [];
|
||||
benchmarkTimings[description].push(duration);
|
||||
});
|
||||
}
|
||||
|
||||
const results = {};
|
||||
for (let benchmarkName in benchmarkTimings) {
|
||||
results[benchmarkName] =
|
||||
benchmarkTimings[benchmarkName].reduce((sum, value) => sum + value, 0) /
|
||||
benchmarksCount;
|
||||
}
|
||||
return results;
|
||||
/** @type {{[benchmarkName: string]: number}} */
|
||||
const results = {};
|
||||
for (const benchmarkName in benchmarkTimings) {
|
||||
reportBenchmarkResult({
|
||||
name: benchmarkName,
|
||||
samplesMs: benchmarkTimings[benchmarkName],
|
||||
});
|
||||
results[benchmarkName] =
|
||||
benchmarkTimings[benchmarkName].reduce(
|
||||
(sum, value) => sum + value,
|
||||
0
|
||||
) / benchmarksCount;
|
||||
}
|
||||
return results;
|
||||
},
|
||||
};
|
||||
return suite;
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('gdjs.Polygon', function() {
|
||||
|
||||
const benchmarkSuite = makeBenchmarkSuite({
|
||||
benchmarksCount: 20,
|
||||
iterationsCount: 30000,
|
||||
iterationsCount: 120000,
|
||||
});
|
||||
benchmarkSuite
|
||||
.add('collisionTest between two overlapping rectangles', i => {
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('gdjs.RuntimeObject', function() {
|
||||
|
||||
const benchmarkSuite = makeBenchmarkSuite({
|
||||
benchmarksCount: 60,
|
||||
iterationsCount: 60000,
|
||||
iterationsCount: 180000,
|
||||
});
|
||||
benchmarkSuite
|
||||
.add('getAABB of a non rotated, default center', (i) => {
|
||||
@@ -37,7 +37,7 @@ describe('gdjs.RuntimeObject', function() {
|
||||
|
||||
const benchmarkSuite = makeBenchmarkSuite({
|
||||
benchmarksCount: 60,
|
||||
iterationsCount: 60000,
|
||||
iterationsCount: 180000,
|
||||
});
|
||||
benchmarkSuite
|
||||
.add('getAABB of a non rotated, non default center', (i) => {
|
||||
|
||||
@@ -0,0 +1,715 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* Scene benchmarks: scenes run like in a game (objects, behaviors, events
|
||||
* and rendering), measured by the gameplay test harness (`harness.benchmark`).
|
||||
* Events are written like the code generated for them.
|
||||
*/
|
||||
describe('Scene benchmarks', function () {
|
||||
const imageResourceName = 'base/GDJS/tests/tests-utils/assets/64x64.jpg';
|
||||
// A small resolution and small objects: on machines without GPU, pixels
|
||||
// are drawn by the CPU, which is slow and is not the work of the engine
|
||||
// being measured (it does not depend on the size of objects).
|
||||
const gameWidth = 320;
|
||||
const gameHeight = 240;
|
||||
|
||||
/** @param {number} ms */
|
||||
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
/**
|
||||
* @param {gdjs.RuntimeScene} runtimeScene
|
||||
* @param {string} objectName
|
||||
*/
|
||||
const getObjects = (runtimeScene, objectName) =>
|
||||
runtimeScene.getObjects(objectName) || [];
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Array<BehaviorData & any>} behaviors
|
||||
* @returns {gdjs.SpriteObjectData}
|
||||
*/
|
||||
const createSpriteObjectData = (name, behaviors = []) => ({
|
||||
name,
|
||||
type: 'Sprite',
|
||||
variables: [],
|
||||
behaviors,
|
||||
effects: [],
|
||||
updateIfNotVisible: false,
|
||||
animations: [
|
||||
{
|
||||
name: 'Idle',
|
||||
useMultipleDirections: false,
|
||||
directions: [
|
||||
{
|
||||
timeBetweenFrames: 0,
|
||||
looping: false,
|
||||
sprites: [
|
||||
{
|
||||
image: imageResourceName,
|
||||
points: [],
|
||||
originPoint: { name: 'origine', x: 0, y: 0 },
|
||||
centerPoint: { name: 'centre', x: 0, y: 0, automatic: true },
|
||||
hasCustomCollisionMask: false,
|
||||
customCollisionMask: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {gdjs.TextObjectData}
|
||||
*/
|
||||
const createTextObjectData = (name) => ({
|
||||
name,
|
||||
type: 'TextObject::Text',
|
||||
variables: [],
|
||||
behaviors: [],
|
||||
effects: [],
|
||||
content: {
|
||||
characterSize: 12,
|
||||
font: '',
|
||||
bold: false,
|
||||
italic: false,
|
||||
underlined: false,
|
||||
color: '255;255;255',
|
||||
text: 'Score: 0',
|
||||
textAlignment: 'left',
|
||||
verticalTextAlignment: 'top',
|
||||
lineHeight: 0,
|
||||
isOutlineEnabled: false,
|
||||
outlineThickness: 0,
|
||||
outlineColor: '0;0;0',
|
||||
isShadowEnabled: false,
|
||||
shadowColor: '0;0;0',
|
||||
shadowOpacity: 0,
|
||||
shadowDistance: 0,
|
||||
shadowAngle: 0,
|
||||
shadowBlurRadius: 0,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Array<BehaviorData & any>} behaviors
|
||||
* @returns {gdjs.Cube3DObjectData}
|
||||
*/
|
||||
const createCubeObjectData = (name, behaviors) =>
|
||||
/** @type {gdjs.Cube3DObjectData} */ ({
|
||||
name,
|
||||
type: 'Scene3D::Cube3DObject',
|
||||
variables: [],
|
||||
behaviors,
|
||||
effects: [],
|
||||
content: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
depth: 16,
|
||||
enableTextureTransparency: false,
|
||||
facesOrientation: 'Y',
|
||||
frontFaceResourceName: imageResourceName,
|
||||
backFaceResourceName: imageResourceName,
|
||||
backFaceUpThroughWhichAxisRotation: 'X',
|
||||
leftFaceResourceName: imageResourceName,
|
||||
rightFaceResourceName: imageResourceName,
|
||||
topFaceResourceName: imageResourceName,
|
||||
bottomFaceResourceName: imageResourceName,
|
||||
frontFaceResourceRepeat: false,
|
||||
backFaceResourceRepeat: false,
|
||||
leftFaceResourceRepeat: false,
|
||||
rightFaceResourceRepeat: false,
|
||||
topFaceResourceRepeat: false,
|
||||
bottomFaceResourceRepeat: false,
|
||||
tileScale: 1,
|
||||
frontFaceVisible: true,
|
||||
backFaceVisible: true,
|
||||
leftFaceVisible: true,
|
||||
rightFaceVisible: true,
|
||||
topFaceVisible: true,
|
||||
bottomFaceVisible: true,
|
||||
tint: '255;255;255',
|
||||
isCastingShadow: false,
|
||||
isReceivingShadow: false,
|
||||
materialType: 'Basic',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {{x: number, y: number, z?: number, width: number, height: number, depth?: number}} instance
|
||||
* @returns {InstanceData}
|
||||
*/
|
||||
const createInstance = (name, { x, y, z, width, height, depth }) => ({
|
||||
persistentUuid: '',
|
||||
layer: '',
|
||||
name,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
angle: 0,
|
||||
zOrder: 0,
|
||||
customSize: true,
|
||||
width,
|
||||
height,
|
||||
depth,
|
||||
numberProperties: [],
|
||||
stringProperties: [],
|
||||
initialVariables: [],
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {number} count
|
||||
* @param {{width: number, height: number, areaWidth?: number, areaHeight?: number, minY?: number}} options
|
||||
* @param {() => number} random
|
||||
*/
|
||||
const createRandomInstances = (name, count, options, random) => {
|
||||
const areaWidth = options.areaWidth || gameWidth;
|
||||
const areaHeight = options.areaHeight || gameHeight;
|
||||
const instances = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
instances.push(
|
||||
createInstance(name, {
|
||||
x: random() * (areaWidth - options.width),
|
||||
y: (options.minY || 0) + random() * (areaHeight - options.height),
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
})
|
||||
);
|
||||
}
|
||||
return instances;
|
||||
};
|
||||
|
||||
/**
|
||||
* Run the scene with the gameplay test harness, and report the measures of
|
||||
* `harness.benchmark`.
|
||||
* @param {{
|
||||
* name: string,
|
||||
* objects: Array<ObjectData>,
|
||||
* instances: Array<InstanceData>,
|
||||
* behaviorsSharedData?: Array<BehaviorSharedData & any>,
|
||||
* eventsFunction?: (runtimeScene: gdjs.RuntimeScene) => void,
|
||||
* renderingType?: '' | '2d' | '3d' | '2d+3d',
|
||||
* warmupFrames?: number,
|
||||
* frames?: number,
|
||||
* }} scene
|
||||
*/
|
||||
const runSceneBenchmark = async ({
|
||||
name,
|
||||
objects,
|
||||
instances,
|
||||
behaviorsSharedData,
|
||||
eventsFunction,
|
||||
renderingType,
|
||||
warmupFrames,
|
||||
frames,
|
||||
}) => {
|
||||
// The physics engines are loaded asynchronously, and needed even by games
|
||||
// not using them (to dispose them).
|
||||
for (let i = 0; i < 400 && !(window.Box2D && window.Jolt); i++) {
|
||||
await delay(5);
|
||||
}
|
||||
const mangledName = 'SceneBenchmark' + name.replace(/[^A-Za-z0-9]/g, '');
|
||||
// Where the events of a scene are found (see `setEventsGeneratedCodeFunction`).
|
||||
// @ts-ignore
|
||||
gdjs[mangledName + 'Code'] = { func: eventsFunction || (() => {}) };
|
||||
const runtimeGame = new gdjs.RuntimeGame(
|
||||
gdjs.createProjectData({
|
||||
layouts: [
|
||||
{
|
||||
r: 0,
|
||||
v: 0,
|
||||
b: 0,
|
||||
mangledName,
|
||||
name,
|
||||
stopSoundsOnStartup: false,
|
||||
title: '',
|
||||
behaviorsSharedData: behaviorsSharedData || [],
|
||||
objects,
|
||||
objectsGroups: [],
|
||||
instances,
|
||||
layers: [
|
||||
{
|
||||
name: '',
|
||||
renderingType,
|
||||
visibility: true,
|
||||
cameras: [],
|
||||
effects: [],
|
||||
ambientLightColorR: 200,
|
||||
ambientLightColorG: 200,
|
||||
ambientLightColorB: 200,
|
||||
isLightingLayer: false,
|
||||
followBaseLayerCamera: false,
|
||||
},
|
||||
],
|
||||
variables: [],
|
||||
usedResources: [],
|
||||
renderer3DWorldScale: 100,
|
||||
uiSettings: {
|
||||
grid: false,
|
||||
gridType: 'rectangular',
|
||||
gridWidth: 10,
|
||||
gridHeight: 10,
|
||||
gridOffsetX: 0,
|
||||
gridOffsetY: 0,
|
||||
gridColor: 0,
|
||||
gridAlpha: 1,
|
||||
snap: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
resources: {
|
||||
resources: [
|
||||
{
|
||||
kind: 'image',
|
||||
name: imageResourceName,
|
||||
metadata: '',
|
||||
file: imageResourceName,
|
||||
userAdded: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
propertiesOverrides: {
|
||||
windowWidth: gameWidth,
|
||||
windowHeight: gameHeight,
|
||||
},
|
||||
})
|
||||
);
|
||||
// The assets of the scene are loaded by `harness.goToScene`.
|
||||
runtimeGame.getRenderer().createStandardCanvas(document.body);
|
||||
|
||||
const result = await gdjs.gameplayTests.runGameplayTest(runtimeGame, {
|
||||
testName: name,
|
||||
timeoutMs: 120000,
|
||||
maxScreenshots: 0,
|
||||
source: [
|
||||
'harness.setRandomSeed(1);',
|
||||
`await harness.goToScene(${JSON.stringify(name)});`,
|
||||
`await harness.benchmark(${JSON.stringify(name)}, ${JSON.stringify({
|
||||
warmupFrames: warmupFrames || 30,
|
||||
frames: frames || 200,
|
||||
})});`,
|
||||
].join('\n'),
|
||||
});
|
||||
runtimeGame.dispose(true);
|
||||
// @ts-ignore
|
||||
delete gdjs[mangledName + 'Code'];
|
||||
|
||||
expect(result.errors).to.eql([]);
|
||||
expect(result.status).to.be('passed');
|
||||
const benchmarkResult = result.benchmarks[0];
|
||||
reportBenchmarkResult({
|
||||
name: 'Scene: ' + name,
|
||||
samplesMs: benchmarkResult.frameTimesMs,
|
||||
counters: {
|
||||
drawCallsPerFrame: benchmarkResult.drawCallsPerFrame,
|
||||
objectsCount: benchmarkResult.objectsCount,
|
||||
stateChecksum: benchmarkResult.stateChecksum,
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
`${name}: ${benchmarkResult.medianFrameTimeMs}ms per frame (median), ` +
|
||||
`${benchmarkResult.drawCallsPerFrame} draw calls per frame, ` +
|
||||
`${benchmarkResult.objectsCount} objects. Slowest sections: ` +
|
||||
benchmarkResult.sections
|
||||
.slice(0, 4)
|
||||
.map((section) => `${section.name} ${section.avgTimeMs}ms`)
|
||||
.join(', ')
|
||||
);
|
||||
};
|
||||
|
||||
it('benchmark static sprites', async function () {
|
||||
this.timeout(120000);
|
||||
await runSceneBenchmark({
|
||||
name: 'Static sprites (10000)',
|
||||
objects: [createSpriteObjectData('Tile')],
|
||||
instances: createRandomInstances(
|
||||
'Tile',
|
||||
10000,
|
||||
{ width: 4, height: 4 },
|
||||
makeSeededRandom(1)
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it('benchmark moving and rotating sprites', async function () {
|
||||
this.timeout(120000);
|
||||
await runSceneBenchmark({
|
||||
name: 'Moving and rotating sprites (5000)',
|
||||
objects: [createSpriteObjectData('Ship')],
|
||||
instances: createRandomInstances(
|
||||
'Ship',
|
||||
5000,
|
||||
{ width: 4, height: 4 },
|
||||
makeSeededRandom(1)
|
||||
),
|
||||
eventsFunction: (runtimeScene) => {
|
||||
for (const ship of getObjects(runtimeScene, 'Ship')) {
|
||||
ship.rotate(90);
|
||||
ship.addPolarForce(ship.getAngle(), 60, 0);
|
||||
ship.setPosition(
|
||||
(ship.getX() + gameWidth) % gameWidth,
|
||||
(ship.getY() + gameHeight) % gameHeight
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('benchmark collisions and separation', async function () {
|
||||
this.timeout(120000);
|
||||
const random = makeSeededRandom(1);
|
||||
const area = { width: 10, height: 10, areaWidth: 640, areaHeight: 480 };
|
||||
const enemies = [];
|
||||
const obstacles = [];
|
||||
const enemiesLists = Hashtable.newFrom({ Enemy: enemies });
|
||||
const obstaclesLists = Hashtable.newFrom({ Obstacle: obstacles });
|
||||
await runSceneBenchmark({
|
||||
name: 'Collisions and separation (500 vs 500)',
|
||||
objects: [
|
||||
createSpriteObjectData('Enemy'),
|
||||
createSpriteObjectData('Obstacle'),
|
||||
],
|
||||
instances: [
|
||||
...createRandomInstances('Enemy', 500, area, random),
|
||||
...createRandomInstances('Obstacle', 500, area, random),
|
||||
],
|
||||
eventsFunction: (runtimeScene) => {
|
||||
gdjs.copyArray(getObjects(runtimeScene, 'Enemy'), enemies);
|
||||
for (const enemy of enemies) {
|
||||
enemy.addForceTowardPosition(320, 240, 40, 0);
|
||||
}
|
||||
gdjs.copyArray(getObjects(runtimeScene, 'Obstacle'), obstacles);
|
||||
if (
|
||||
gdjs.evtTools.object.hitBoxesCollisionTest(
|
||||
enemiesLists,
|
||||
obstaclesLists,
|
||||
false,
|
||||
runtimeScene,
|
||||
false
|
||||
)
|
||||
) {
|
||||
for (const enemy of enemies) {
|
||||
enemy.separateFromObjectsList(obstaclesLists, false);
|
||||
}
|
||||
}
|
||||
},
|
||||
warmupFrames: 30,
|
||||
frames: 120,
|
||||
});
|
||||
});
|
||||
|
||||
it('benchmark physics 2D bodies', async function () {
|
||||
this.timeout(120000);
|
||||
const physicsBehavior = {
|
||||
name: 'Physics2',
|
||||
type: 'Physics2::Physics2Behavior',
|
||||
bodyType: 'Dynamic',
|
||||
bullet: false,
|
||||
fixedRotation: false,
|
||||
canSleep: true,
|
||||
shape: 'Box',
|
||||
shapeDimensionA: 0,
|
||||
shapeDimensionB: 0,
|
||||
shapeOffsetX: 0,
|
||||
shapeOffsetY: 0,
|
||||
polygonOrigin: 'Center',
|
||||
vertices: [],
|
||||
density: 1,
|
||||
friction: 0.3,
|
||||
restitution: 0.1,
|
||||
linearDamping: 0,
|
||||
angularDamping: 0.1,
|
||||
gravityScale: 1,
|
||||
layers: 1,
|
||||
masks: 1,
|
||||
};
|
||||
await runSceneBenchmark({
|
||||
name: 'Physics 2D (500 bodies)',
|
||||
objects: [
|
||||
createSpriteObjectData('Box', [physicsBehavior]),
|
||||
createSpriteObjectData('Ground', [
|
||||
{ ...physicsBehavior, bodyType: 'Static' },
|
||||
]),
|
||||
],
|
||||
instances: [
|
||||
...createRandomInstances(
|
||||
'Box',
|
||||
500,
|
||||
{ width: 8, height: 8, areaHeight: 400, minY: -400 },
|
||||
makeSeededRandom(1)
|
||||
),
|
||||
createInstance('Ground', {
|
||||
x: 0,
|
||||
y: gameHeight - 16,
|
||||
width: gameWidth,
|
||||
height: 16,
|
||||
}),
|
||||
],
|
||||
behaviorsSharedData: [
|
||||
{
|
||||
name: 'Physics2',
|
||||
type: 'Physics2::Physics2Behavior',
|
||||
gravityX: 0,
|
||||
gravityY: 9.8,
|
||||
scaleX: 100,
|
||||
scaleY: 100,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('benchmark physics 3D bodies', async function () {
|
||||
this.timeout(120000);
|
||||
const physicsBehavior = {
|
||||
name: 'Physics3D',
|
||||
type: 'Physics3D::Physics3DBehavior',
|
||||
bodyType: 'Dynamic',
|
||||
bullet: false,
|
||||
fixedRotation: false,
|
||||
shape: 'Box',
|
||||
meshShapeResourceName: '',
|
||||
shapeOrientation: 'Z',
|
||||
shapeDimensionA: 0,
|
||||
shapeDimensionB: 0,
|
||||
shapeDimensionC: 0,
|
||||
shapeOffsetX: 0,
|
||||
shapeOffsetY: 0,
|
||||
shapeOffsetZ: 0,
|
||||
massCenterOffsetX: 0,
|
||||
massCenterOffsetY: 0,
|
||||
massCenterOffsetZ: 0,
|
||||
massOverride: 0,
|
||||
density: 1,
|
||||
friction: 0.3,
|
||||
restitution: 0.1,
|
||||
linearDamping: 0.1,
|
||||
angularDamping: 0.1,
|
||||
gravityScale: 1,
|
||||
layers: (1 << 4) | (1 << 0),
|
||||
masks: (1 << 4) | (1 << 0),
|
||||
};
|
||||
const random = makeSeededRandom(1);
|
||||
const boxes = [];
|
||||
for (let i = 0; i < 500; i++) {
|
||||
boxes.push(
|
||||
createInstance('Box', {
|
||||
x: random() * gameWidth,
|
||||
y: random() * gameHeight,
|
||||
z: 50 + random() * 800,
|
||||
width: 16,
|
||||
height: 16,
|
||||
depth: 16,
|
||||
})
|
||||
);
|
||||
}
|
||||
await runSceneBenchmark({
|
||||
name: 'Physics 3D (500 bodies)',
|
||||
objects: [
|
||||
createCubeObjectData('Box', [physicsBehavior]),
|
||||
createCubeObjectData('Ground', [
|
||||
{ ...physicsBehavior, bodyType: 'Static' },
|
||||
]),
|
||||
],
|
||||
instances: [
|
||||
...boxes,
|
||||
createInstance('Ground', {
|
||||
x: -100,
|
||||
y: -100,
|
||||
z: -16,
|
||||
width: gameWidth + 200,
|
||||
height: gameHeight + 200,
|
||||
depth: 16,
|
||||
}),
|
||||
],
|
||||
renderingType: '3d',
|
||||
behaviorsSharedData: [
|
||||
{
|
||||
name: 'Physics3D',
|
||||
type: 'Physics3D::Physics3DBehavior',
|
||||
gravityX: 0,
|
||||
gravityY: 0,
|
||||
gravityZ: -9.8,
|
||||
worldScale: 100,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('benchmark platformer characters', async function () {
|
||||
this.timeout(120000);
|
||||
const platforms = [];
|
||||
for (let x = 0; x < gameWidth; x += 32) {
|
||||
platforms.push(
|
||||
createInstance('Platform', {
|
||||
x,
|
||||
y: gameHeight - 16,
|
||||
width: 32,
|
||||
height: 16,
|
||||
})
|
||||
);
|
||||
}
|
||||
for (let x = 16; x < gameWidth; x += 96) {
|
||||
platforms.push(
|
||||
createInstance('Platform', {
|
||||
x,
|
||||
y: gameHeight - 80,
|
||||
width: 48,
|
||||
height: 8,
|
||||
})
|
||||
);
|
||||
}
|
||||
await runSceneBenchmark({
|
||||
name: 'Platformer characters (300)',
|
||||
objects: [
|
||||
createSpriteObjectData('Player', [
|
||||
{
|
||||
type: 'PlatformBehavior::PlatformerObjectBehavior',
|
||||
name: 'PlatformerObject',
|
||||
gravity: 1000,
|
||||
maxFallingSpeed: 700,
|
||||
acceleration: 1500,
|
||||
deceleration: 1500,
|
||||
maxSpeed: 150,
|
||||
jumpSpeed: 400,
|
||||
canGrabPlatforms: false,
|
||||
ignoreDefaultControls: true,
|
||||
slopeMaxAngle: 60,
|
||||
jumpSustainTime: 0.2,
|
||||
useLegacyTrajectory: false,
|
||||
useRepeatedJump: false,
|
||||
},
|
||||
]),
|
||||
createSpriteObjectData('Platform', [
|
||||
{
|
||||
type: 'PlatformBehavior::PlatformBehavior',
|
||||
name: 'Platform',
|
||||
platformType: 'NormalPlatform',
|
||||
canBeGrabbed: false,
|
||||
yGrabOffset: 0,
|
||||
},
|
||||
]),
|
||||
],
|
||||
instances: [
|
||||
...createRandomInstances(
|
||||
'Player',
|
||||
300,
|
||||
{ width: 8, height: 16, areaHeight: gameHeight - 100 },
|
||||
makeSeededRandom(1)
|
||||
),
|
||||
...platforms,
|
||||
],
|
||||
eventsFunction: (runtimeScene) => {
|
||||
const timeFromStart = runtimeScene.getTimeManager().getTimeFromStart();
|
||||
const isGoingRight = Math.floor(timeFromStart / 2000) % 2 === 0;
|
||||
const isJumping = Math.floor(timeFromStart / 500) % 3 === 0;
|
||||
for (const player of getObjects(runtimeScene, 'Player')) {
|
||||
const platformer =
|
||||
/** @type {gdjs.PlatformerObjectRuntimeBehavior} */ (
|
||||
player.getBehavior('PlatformerObject')
|
||||
);
|
||||
if (isGoingRight) platformer.simulateRightKey();
|
||||
else platformer.simulateLeftKey();
|
||||
if (isJumping && player.id % 2 === 0) platformer.simulateJumpKey();
|
||||
if (player.getY() > gameHeight) player.setPosition(player.getX(), 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('benchmark text updates', async function () {
|
||||
this.timeout(120000);
|
||||
await runSceneBenchmark({
|
||||
name: 'Text updates (1000)',
|
||||
objects: [createTextObjectData('Score')],
|
||||
instances: createRandomInstances(
|
||||
'Score',
|
||||
1000,
|
||||
{ width: 60, height: 12 },
|
||||
makeSeededRandom(1)
|
||||
),
|
||||
eventsFunction: (runtimeScene) => {
|
||||
const score = Math.floor(
|
||||
runtimeScene.getTimeManager().getTimeFromStart() / 10
|
||||
);
|
||||
const texts = getObjects(runtimeScene, 'Score');
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
/** @type {gdjs.TextRuntimeObject} */ (texts[i]).setText(
|
||||
'Score: ' + gdjs.evtTools.common.toString(score + i)
|
||||
);
|
||||
}
|
||||
},
|
||||
warmupFrames: 10,
|
||||
frames: 40,
|
||||
});
|
||||
});
|
||||
|
||||
it('benchmark objects creation and deletion', async function () {
|
||||
this.timeout(120000);
|
||||
await runSceneBenchmark({
|
||||
name: 'Objects creation and deletion (100 per frame)',
|
||||
objects: [createSpriteObjectData('Bullet')],
|
||||
instances: [],
|
||||
eventsFunction: (runtimeScene) => {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const bullet = runtimeScene.createObject('Bullet');
|
||||
if (bullet) {
|
||||
bullet.setPosition(
|
||||
Math.random() * gameWidth,
|
||||
Math.random() * gameHeight
|
||||
);
|
||||
bullet.setWidth(8);
|
||||
bullet.setHeight(8);
|
||||
}
|
||||
}
|
||||
const bullets = getObjects(runtimeScene, 'Bullet');
|
||||
if (bullets.length > 2000) {
|
||||
for (const bullet of bullets.slice(0, 100)) {
|
||||
bullet.deleteFromScene();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('benchmark heavy events', async function () {
|
||||
this.timeout(120000);
|
||||
await runSceneBenchmark({
|
||||
name: 'Heavy events (variables, loops, strings)',
|
||||
objects: [createSpriteObjectData('Unit')],
|
||||
instances: createRandomInstances(
|
||||
'Unit',
|
||||
1000,
|
||||
{ width: 8, height: 8 },
|
||||
makeSeededRandom(1)
|
||||
),
|
||||
eventsFunction: (runtimeScene) => {
|
||||
const data = runtimeScene.getVariables().get('Data');
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
const item = data.getChild('Item' + gdjs.evtTools.common.toString(i));
|
||||
item.setNumber(item.getAsNumber() * 0.99 + Math.sin(i));
|
||||
}
|
||||
const log = runtimeScene.getVariables().get('Log');
|
||||
log.pushValue(
|
||||
'Frame ' + gdjs.evtTools.common.toString(data.getChildrenCount())
|
||||
);
|
||||
if (log.getChildrenCount() > 100) log.removeAtIndex(0);
|
||||
for (const unit of getObjects(runtimeScene, 'Unit')) {
|
||||
const health = unit.getVariables().get('Health');
|
||||
health.add(1);
|
||||
if (health.getAsNumber() > 100) {
|
||||
health.setNumber(0);
|
||||
unit.setX((unit.getX() + 1) % gameWidth);
|
||||
}
|
||||
unit
|
||||
.getVariables()
|
||||
.get('Name')
|
||||
.setString('Unit ' + gdjs.evtTools.common.toString(unit.id));
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ describe('gdjs.VariablesContainer', function() {
|
||||
this.timeout(20000);
|
||||
var container = new gdjs.VariablesContainer();
|
||||
|
||||
const benchmarkSuite = makeBenchmarkSuite();
|
||||
const benchmarkSuite = makeBenchmarkSuite({ iterationsCount: 400000 });
|
||||
benchmarkSuite
|
||||
.add('get', () => {
|
||||
container.get('Var1');
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* Compare the benchmarks (`GDJS/tests/benchmarks`) of the current version of
|
||||
* the game engine with a base version (by default, where the branch started
|
||||
* from `origin/master`).
|
||||
*
|
||||
* Both versions are built, then the benchmarks are run alternately on each
|
||||
* version, on the same machine: the differences of speed between machines
|
||||
* (or a machine slowing down during the run) don't affect the comparison.
|
||||
*
|
||||
* Usage: node compare-benchmarks.js [--base-ref=origin/master] [--rounds=3]
|
||||
* [--grep=<benchmark name pattern>] [--markdown=<file>] [--json=<file>]
|
||||
* [--browser=ChromeHeadlessBenchmark]
|
||||
*/
|
||||
const { execFileSync, spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const args = require('minimist')(process.argv.slice(2), {
|
||||
string: ['base-ref', 'grep', 'markdown', 'json', 'browser'],
|
||||
default: {
|
||||
'base-ref': 'origin/master',
|
||||
rounds: 3,
|
||||
browser: 'ChromeHeadlessBenchmark',
|
||||
},
|
||||
});
|
||||
|
||||
/** A change smaller than this is never reported as significant. */
|
||||
const MINIMUM_SIGNIFICANT_CHANGE = 0.05;
|
||||
|
||||
const repositoryPath = path.join(__dirname, '../..');
|
||||
// Relative to the repository root, as expected by `karma.conf.js`.
|
||||
const builtRuntimesPath = 'GDJS/tests/.benchmark-runtimes';
|
||||
const resultsPath = path.join(repositoryPath, builtRuntimesPath, 'results');
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* name: string,
|
||||
* samplesMs: Array<number>,
|
||||
* counters?: {[counterName: string]: number | string | null},
|
||||
* }} BenchmarkResult
|
||||
* @typedef {'base' | 'head'} Version
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Array<string>} gitArgs
|
||||
* @param {string=} cwd
|
||||
*/
|
||||
const git = (gitArgs, cwd = repositoryPath) =>
|
||||
execFileSync('git', gitArgs, { cwd, encoding: 'utf8' }).trim();
|
||||
|
||||
/**
|
||||
* @param {string} gdjsPath The `GDJS` folder of the version to build.
|
||||
* @param {Version} version
|
||||
*/
|
||||
const buildRuntime = (gdjsPath, version) => {
|
||||
const outPath = path.join(repositoryPath, builtRuntimesPath, version);
|
||||
fs.rmSync(outPath, { recursive: true, force: true });
|
||||
execFileSync('node', ['scripts/build.js', '--out', outPath], {
|
||||
cwd: gdjsPath,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
};
|
||||
|
||||
/** @param {string} baseCommit */
|
||||
const buildBaseRuntime = (baseCommit) => {
|
||||
const worktreePath = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'gdjs-benchmark-base-')
|
||||
);
|
||||
git(['worktree', 'add', '--detach', worktreePath, baseCommit]);
|
||||
try {
|
||||
// The dependencies of the current version are reused (they rarely change
|
||||
// and are only used to build).
|
||||
fs.symlinkSync(
|
||||
path.join(repositoryPath, 'GDJS/node_modules'),
|
||||
path.join(worktreePath, 'GDJS/node_modules'),
|
||||
'dir'
|
||||
);
|
||||
buildRuntime(path.join(worktreePath, 'GDJS'), 'base');
|
||||
} finally {
|
||||
git(['worktree', 'remove', '--force', worktreePath]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Version} version
|
||||
* @param {number} round
|
||||
* @returns {Array<BenchmarkResult>}
|
||||
*/
|
||||
const runBenchmarks = (version, round) => {
|
||||
const resultsFile = path.join(resultsPath, `${version}-${round}.json`);
|
||||
console.log(`\n⏱️ Running the benchmarks on ${version} (round ${round})...`);
|
||||
const { status } = spawnSync(
|
||||
'npx',
|
||||
[
|
||||
'karma',
|
||||
'start',
|
||||
'--single-run',
|
||||
`--browsers=${args.browser}`,
|
||||
'--benchmarksOnly',
|
||||
`--gdjsRuntimePath=${builtRuntimesPath}/${version}`,
|
||||
`--benchmarkResultsFile=${resultsFile}`,
|
||||
...(args.grep ? [`--grep=${args.grep}`] : []),
|
||||
],
|
||||
{ cwd: __dirname, stdio: 'inherit' }
|
||||
);
|
||||
if (status !== 0) {
|
||||
console.warn(
|
||||
`⚠️ Some benchmarks failed on ${version} (round ${round}): they are reported as missing.`
|
||||
);
|
||||
}
|
||||
return fs.existsSync(resultsFile)
|
||||
? JSON.parse(fs.readFileSync(resultsFile, 'utf8'))
|
||||
: [];
|
||||
};
|
||||
|
||||
/** @param {Array<number>} values */
|
||||
const median = (values) => {
|
||||
const sortedValues = [...values].sort((a, b) => a - b);
|
||||
const middle = Math.floor(sortedValues.length / 2);
|
||||
return sortedValues.length % 2
|
||||
? sortedValues[middle]
|
||||
: (sortedValues[middle - 1] + sortedValues[middle]) / 2;
|
||||
};
|
||||
|
||||
/** @param {number} change */
|
||||
const formatChange = (change) =>
|
||||
(change > 0 ? '+' : '') + (change * 100).toFixed(1) + '%';
|
||||
|
||||
/** @param {number} timeMs */
|
||||
const formatTime = (timeMs) =>
|
||||
timeMs >= 10 ? timeMs.toFixed(1) + 'ms' : timeMs.toFixed(3) + 'ms';
|
||||
|
||||
/**
|
||||
* Counters don't depend on the machine: compare them exactly. A counter
|
||||
* differing between rounds of the same version is not deterministic.
|
||||
* @param {Array<BenchmarkResult>} baseResults
|
||||
* @param {Array<BenchmarkResult>} headResults
|
||||
* @returns {Array<{name: string, base: string, head: string}>}
|
||||
*/
|
||||
const compareCounters = (baseResults, headResults) => {
|
||||
const counterNames = new Set(
|
||||
[...baseResults, ...headResults].flatMap((result) =>
|
||||
Object.keys(result.counters || {})
|
||||
)
|
||||
);
|
||||
return [...counterNames].map((counterName) => {
|
||||
/** @param {Array<BenchmarkResult>} results */
|
||||
const formatCounter = (results) => {
|
||||
const values = [
|
||||
...new Set(
|
||||
results.map((result) => String((result.counters || {})[counterName]))
|
||||
),
|
||||
];
|
||||
if (!values.length) return '-';
|
||||
return values.length === 1
|
||||
? values[0]
|
||||
: values.join(' / ') + ' (not deterministic)';
|
||||
};
|
||||
return {
|
||||
name: counterName,
|
||||
base: formatCounter(baseResults),
|
||||
head: formatCounter(headResults),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{[version: string]: Array<Array<BenchmarkResult>>}} resultsByVersionAndRound
|
||||
*/
|
||||
const compareResults = (resultsByVersionAndRound) => {
|
||||
/** @type {Array<string>} */
|
||||
const benchmarkNames = [];
|
||||
for (const version of ['base', 'head']) {
|
||||
for (const roundResults of resultsByVersionAndRound[version]) {
|
||||
for (const { name } of roundResults) {
|
||||
if (!benchmarkNames.includes(name)) benchmarkNames.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return benchmarkNames.map((name) => {
|
||||
/** @param {Version} version */
|
||||
const getRoundResults = (version) =>
|
||||
resultsByVersionAndRound[version]
|
||||
.map((roundResults) =>
|
||||
roundResults.find((result) => result.name === name)
|
||||
)
|
||||
.filter(Boolean);
|
||||
const baseResults = getRoundResults('base');
|
||||
const headResults = getRoundResults('head');
|
||||
// Each round is summarized by its median: robust to a few slow samples
|
||||
// (garbage collection, the machine doing something else...).
|
||||
const baseMediansMs = baseResults.map((result) => median(result.samplesMs));
|
||||
const headMediansMs = headResults.map((result) => median(result.samplesMs));
|
||||
if (!baseMediansMs.length || !headMediansMs.length) {
|
||||
return {
|
||||
name,
|
||||
baseMs: baseMediansMs.length ? median(baseMediansMs) : null,
|
||||
headMs: headMediansMs.length ? median(headMediansMs) : null,
|
||||
change: null,
|
||||
changeRange: null,
|
||||
verdict: baseMediansMs.length ? 'removed or failing' : 'new',
|
||||
counters: compareCounters(baseResults, headResults),
|
||||
};
|
||||
}
|
||||
|
||||
const baseMs = median(baseMediansMs);
|
||||
const headMs = median(headMediansMs);
|
||||
const change = headMs / baseMs - 1;
|
||||
// The changes measured between every round of the base and every round
|
||||
// of the head: the change is significant only if all agree.
|
||||
const roundChanges = baseMediansMs.flatMap((baseMedianMs) =>
|
||||
headMediansMs.map((headMedianMs) => headMedianMs / baseMedianMs - 1)
|
||||
);
|
||||
const changeRange = [Math.min(...roundChanges), Math.max(...roundChanges)];
|
||||
const isSignificant =
|
||||
Math.abs(change) >= MINIMUM_SIGNIFICANT_CHANGE &&
|
||||
(changeRange[0] > 0 || changeRange[1] < 0);
|
||||
|
||||
return {
|
||||
name,
|
||||
baseMs,
|
||||
headMs,
|
||||
change,
|
||||
changeRange,
|
||||
verdict: !isSignificant ? 'unchanged' : change < 0 ? 'faster' : 'slower',
|
||||
counters: compareCounters(baseResults, headResults),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {ReturnType<typeof compareResults>} comparisons
|
||||
* @param {{baseCommit: string, headCommit: string, rounds: number}} context
|
||||
*/
|
||||
const makeMarkdownReport = (
|
||||
comparisons,
|
||||
{ baseCommit, headCommit, rounds }
|
||||
) => {
|
||||
const verdictEmojis = {
|
||||
faster: '🚀 faster',
|
||||
slower: '🐢 slower',
|
||||
unchanged: '≈',
|
||||
new: '🆕',
|
||||
'removed or failing': '⚠️ removed or failing',
|
||||
};
|
||||
const lines = [
|
||||
'<!-- benchmark-results -->',
|
||||
'### ⏱️ Game engine benchmarks',
|
||||
'',
|
||||
`Comparing \`${headCommit.slice(0, 8)}\` to its base \`${baseCommit.slice(0, 8)}\` ` +
|
||||
'(on a pull request, the version tested is the pull request merged into its base), ' +
|
||||
`run alternately ${rounds} times on the same machine (${os.cpus()[0].model}, ` +
|
||||
`${os.cpus().length} cores, rendering without GPU).`,
|
||||
'',
|
||||
'| Benchmark | Base | This version | Change | Range between rounds | |',
|
||||
'|---|---:|---:|---:|---:|---|',
|
||||
...comparisons.map(
|
||||
({ name, baseMs, headMs, change, changeRange, verdict }) =>
|
||||
`| ${name} | ${baseMs === null ? '-' : formatTime(baseMs)} | ` +
|
||||
`${headMs === null ? '-' : formatTime(headMs)} | ` +
|
||||
`${change === null ? '-' : formatChange(change)} | ` +
|
||||
`${changeRange === null ? '-' : changeRange.map(formatChange).join(' to ')} | ` +
|
||||
`${verdictEmojis[verdict]} |`
|
||||
),
|
||||
'',
|
||||
'Times are the median duration of a batch of iterations (micro benchmarks) or of a frame (scene benchmarks). ' +
|
||||
`A change is reported only when larger than ${MINIMUM_SIGNIFICANT_CHANGE * 100}% ` +
|
||||
'and measured by every pair of rounds.',
|
||||
];
|
||||
|
||||
const changedCounters = comparisons.flatMap(({ name, counters }) =>
|
||||
counters
|
||||
.filter(
|
||||
(counter) =>
|
||||
(counter.base !== '-' &&
|
||||
counter.head !== '-' &&
|
||||
counter.base !== counter.head) ||
|
||||
counter.base.includes('not deterministic') ||
|
||||
counter.head.includes('not deterministic')
|
||||
)
|
||||
.map((counter) => ({ benchmarkName: name, ...counter }))
|
||||
);
|
||||
const comparedCountersBenchmarksCount = comparisons.filter(({ counters }) =>
|
||||
counters.some((counter) => counter.base !== '-' && counter.head !== '-')
|
||||
).length;
|
||||
lines.push(
|
||||
'',
|
||||
changedCounters.length
|
||||
? '⚠️ **Counters changed or not deterministic** (draw calls, objects count or final state of a scene):'
|
||||
: comparedCountersBenchmarksCount
|
||||
? `✅ The counters (draw calls, objects count and final state) of the ${comparedCountersBenchmarksCount} scene benchmarks run on both versions are unchanged.`
|
||||
: 'ℹ️ No scene benchmark ran on both versions: their counters (draw calls, objects count and final state) could not be compared.'
|
||||
);
|
||||
if (changedCounters.length) {
|
||||
lines.push(
|
||||
'',
|
||||
'| Benchmark | Counter | Base | This version |',
|
||||
'|---|---|---|---|',
|
||||
...changedCounters.map(
|
||||
({ benchmarkName, name, base, head }) =>
|
||||
`| ${benchmarkName} | ${name} | ${base} | ${head} |`
|
||||
)
|
||||
);
|
||||
}
|
||||
return lines.join('\n') + '\n';
|
||||
};
|
||||
|
||||
const main = () => {
|
||||
const rounds = Number(args.rounds);
|
||||
const headCommit = git(['rev-parse', 'HEAD']);
|
||||
const baseCommit = git(['merge-base', args['base-ref'], 'HEAD']);
|
||||
fs.rmSync(resultsPath, { recursive: true, force: true });
|
||||
fs.mkdirSync(resultsPath, { recursive: true });
|
||||
|
||||
console.log(
|
||||
`🏗️ Building the game engine of this version (${headCommit})...`
|
||||
);
|
||||
buildRuntime(path.join(repositoryPath, 'GDJS'), 'head');
|
||||
console.log(
|
||||
`🏗️ Building the game engine of the base version (${baseCommit})...`
|
||||
);
|
||||
buildBaseRuntime(baseCommit);
|
||||
|
||||
/** @type {{[version: string]: Array<Array<BenchmarkResult>>}} */
|
||||
const resultsByVersionAndRound = { base: [], head: [] };
|
||||
for (let round = 1; round <= rounds; round++) {
|
||||
// Alternate which version runs first, so that a machine getting slower
|
||||
// (or faster) during the run affects both versions the same way.
|
||||
/** @type {Array<Version>} */
|
||||
const versions = round % 2 ? ['base', 'head'] : ['head', 'base'];
|
||||
for (const version of versions) {
|
||||
resultsByVersionAndRound[version].push(runBenchmarks(version, round));
|
||||
}
|
||||
}
|
||||
|
||||
const comparisons = compareResults(resultsByVersionAndRound);
|
||||
const markdownReport = makeMarkdownReport(comparisons, {
|
||||
baseCommit,
|
||||
headCommit,
|
||||
rounds,
|
||||
});
|
||||
console.log('\n' + markdownReport);
|
||||
if (args.markdown) fs.writeFileSync(args.markdown, markdownReport);
|
||||
if (args.json) {
|
||||
fs.writeFileSync(
|
||||
args.json,
|
||||
JSON.stringify({ baseCommit, headCommit, rounds, comparisons }, null, 2)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
main();
|
||||
+198
-152
@@ -1,4 +1,30 @@
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Write the benchmark results sent by the browser (see `reportBenchmarkResult`
|
||||
* in `benchmarks/init.js`) to the file given with `--benchmarkResultsFile`.
|
||||
*/
|
||||
function BenchmarkResultsReporter(config) {
|
||||
const benchmarkResults = [];
|
||||
this.onBrowserInfo = (browser, info) => {
|
||||
if (info && info.benchmarkResult)
|
||||
benchmarkResults.push(info.benchmarkResult);
|
||||
};
|
||||
this.onRunComplete = () => {
|
||||
fs.writeFileSync(
|
||||
config.benchmarkResultsFile,
|
||||
JSON.stringify(benchmarkResults, null, 2)
|
||||
);
|
||||
};
|
||||
}
|
||||
BenchmarkResultsReporter.$inject = ['config'];
|
||||
|
||||
module.exports = function (config) {
|
||||
// The built game engine to test, relative to the repository root. Changed
|
||||
// to compare the benchmarks of two versions (see `compare-benchmarks.js`).
|
||||
const gdjsRuntimePath =
|
||||
config.gdjsRuntimePath || 'newIDE/app/resources/GDJS/Runtime';
|
||||
|
||||
const testFiles = [
|
||||
'./Extensions/TweenBehavior/tests/CubicBezierEasingTestCases.js',
|
||||
'./Extensions/**/tests/**.spec.js',
|
||||
@@ -7,8 +33,11 @@ module.exports = function (config) {
|
||||
|
||||
const benchmarkFiles = [
|
||||
'./GDJS/tests/benchmarks/init.js',
|
||||
'./Extensions/**/benchmark/**.benchmark.js',
|
||||
'./GDJS/tests/benchmarks/**/*.js',
|
||||
'./Extensions/**/benchmarks/**.benchmark.js',
|
||||
'./GDJS/tests/benchmarks/*.js',
|
||||
// Last, so that the micro benchmarks are not affected by what the scenes
|
||||
// leave behind (memory to collect, rendering to finish...).
|
||||
'./GDJS/tests/benchmarks/scenes/*.js',
|
||||
];
|
||||
|
||||
config.set({
|
||||
@@ -34,6 +63,23 @@ module.exports = function (config) {
|
||||
'--use-angle=swiftshader',
|
||||
],
|
||||
},
|
||||
// Same, with the garbage collector exposed so that benchmarks can run
|
||||
// it before measuring.
|
||||
ChromeHeadlessBenchmark: {
|
||||
base: 'ChromeHeadless',
|
||||
flags: [
|
||||
'--no-sandbox',
|
||||
'--disable-gpu',
|
||||
'--use-gl=swiftshader',
|
||||
'--enable-webgl',
|
||||
'--use-angle=swiftshader',
|
||||
'--js-flags=--expose-gc',
|
||||
// Never slow down the timers used to yield between frames.
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
],
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('karma-chrome-launcher'),
|
||||
@@ -41,8 +87,13 @@ module.exports = function (config) {
|
||||
require('karma-firefox-launcher'),
|
||||
require('karma-mocha'),
|
||||
require('karma-sinon'),
|
||||
{ 'reporter:benchmark-results': ['type', BenchmarkResultsReporter] },
|
||||
],
|
||||
reporters: config.benchmarkResultsFile
|
||||
? ['dots', 'benchmark-results']
|
||||
: ['progress'],
|
||||
client: {
|
||||
args: config.grep ? ['--grep', config.grep] : [],
|
||||
mocha: {
|
||||
reporter: 'html',
|
||||
timeout: 10000, // Give a bit more time for CIs (the default 2s can be too low sometimes, as a real browser is involved).
|
||||
@@ -53,190 +104,183 @@ module.exports = function (config) {
|
||||
'/base/tests-utils/': '/base/GDJS/tests/tests-utils/',
|
||||
// Jolt is loaded with a dynamic `import('./jolt-physics.wasm.js')`,
|
||||
// which is resolved relatively to the page, not to the script.
|
||||
'/jolt-physics.wasm.js':
|
||||
'/base/newIDE/app/resources/GDJS/Runtime/Extensions/Physics3DBehavior/jolt-physics.wasm.js',
|
||||
'/jolt-physics.wasm.wasm':
|
||||
'/base/newIDE/app/resources/GDJS/Runtime/Extensions/Physics3DBehavior/jolt-physics.wasm.wasm',
|
||||
'/jolt-physics.wasm.js': `/base/${gdjsRuntimePath}/Extensions/Physics3DBehavior/jolt-physics.wasm.js`,
|
||||
'/jolt-physics.wasm.wasm': `/base/${gdjsRuntimePath}/Extensions/Physics3DBehavior/jolt-physics.wasm.wasm`,
|
||||
},
|
||||
files: [
|
||||
'./GDJS/tests/node_modules/expect.js/index.js',
|
||||
|
||||
//GDJS game engine files: (Order is important)
|
||||
'./newIDE/app/resources/GDJS/Runtime/libs/jshashtable.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/logger.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/gd.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/AsyncTasksManager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/libs/rbush.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/pixi-renderers/pixi.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/pixi-renderers/three.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/pixi-renderers/*.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/howler-sound-manager/howler.min.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/howler-sound-manager/howler-sound-manager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/fontfaceobserver-font-manager/fontfaceobserver.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/fontfaceobserver-font-manager/fontfaceobserver-font-manager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Model3DManager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/jsonmanager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/ResourceLoader.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/ResourceCache.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/timemanager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/polygon.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/runtimeobject.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/RuntimeInstanceContainer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/runtimescene.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/scenestack.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/profiler.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/force.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/RuntimeLayer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/layer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/RuntimeCustomObjectLayer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/timer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/inputmanager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/capturemanager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/named-easings-manager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/runtimegame.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/runtimewatermark.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/variable.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/variablescontainer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/oncetriggers.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/runtimebehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/SpriteAnimator.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/spriteruntimeobject.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/CustomRuntimeObject.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/CustomRuntimeObject2D.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/CustomRuntimeObjectInstanceContainer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/events-tools/commontools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/events-tools/runtimescenetools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/events-tools/inputtools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/events-tools/networktools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/events-tools/objecttools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/events-tools/cameratools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/events-tools/soundtools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/events-tools/storagetools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/indexeddb.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/events-tools/stringtools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/events-tools/windowtools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/debugger-client/abstract-debugger-client.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/debugger-client/hot-reloader.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/gameplay-tests/gameplay-test-runner.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/affinetransformation.js',
|
||||
`${gdjsRuntimePath}/libs/jshashtable.js`,
|
||||
`${gdjsRuntimePath}/logger.js`,
|
||||
`${gdjsRuntimePath}/gd.js`,
|
||||
`${gdjsRuntimePath}/AsyncTasksManager.js`,
|
||||
`${gdjsRuntimePath}/libs/rbush.js`,
|
||||
`${gdjsRuntimePath}/pixi-renderers/pixi.js`,
|
||||
`${gdjsRuntimePath}/pixi-renderers/three.js`,
|
||||
`${gdjsRuntimePath}/pixi-renderers/*.js`,
|
||||
`${gdjsRuntimePath}/howler-sound-manager/howler.min.js`,
|
||||
`${gdjsRuntimePath}/howler-sound-manager/howler-sound-manager.js`,
|
||||
`${gdjsRuntimePath}/fontfaceobserver-font-manager/fontfaceobserver.js`,
|
||||
`${gdjsRuntimePath}/fontfaceobserver-font-manager/fontfaceobserver-font-manager.js`,
|
||||
`${gdjsRuntimePath}/Model3DManager.js`,
|
||||
`${gdjsRuntimePath}/jsonmanager.js`,
|
||||
`${gdjsRuntimePath}/ResourceLoader.js`,
|
||||
`${gdjsRuntimePath}/ResourceCache.js`,
|
||||
`${gdjsRuntimePath}/timemanager.js`,
|
||||
`${gdjsRuntimePath}/polygon.js`,
|
||||
`${gdjsRuntimePath}/runtimeobject.js`,
|
||||
`${gdjsRuntimePath}/RuntimeInstanceContainer.js`,
|
||||
`${gdjsRuntimePath}/runtimescene.js`,
|
||||
`${gdjsRuntimePath}/scenestack.js`,
|
||||
`${gdjsRuntimePath}/profiler.js`,
|
||||
`${gdjsRuntimePath}/force.js`,
|
||||
`${gdjsRuntimePath}/RuntimeLayer.js`,
|
||||
`${gdjsRuntimePath}/layer.js`,
|
||||
`${gdjsRuntimePath}/RuntimeCustomObjectLayer.js`,
|
||||
`${gdjsRuntimePath}/timer.js`,
|
||||
`${gdjsRuntimePath}/inputmanager.js`,
|
||||
`${gdjsRuntimePath}/capturemanager.js`,
|
||||
`${gdjsRuntimePath}/named-easings-manager.js`,
|
||||
`${gdjsRuntimePath}/runtimegame.js`,
|
||||
`${gdjsRuntimePath}/runtimewatermark.js`,
|
||||
`${gdjsRuntimePath}/variable.js`,
|
||||
`${gdjsRuntimePath}/variablescontainer.js`,
|
||||
`${gdjsRuntimePath}/oncetriggers.js`,
|
||||
`${gdjsRuntimePath}/runtimebehavior.js`,
|
||||
`${gdjsRuntimePath}/SpriteAnimator.js`,
|
||||
`${gdjsRuntimePath}/spriteruntimeobject.js`,
|
||||
`${gdjsRuntimePath}/CustomRuntimeObject.js`,
|
||||
`${gdjsRuntimePath}/CustomRuntimeObject2D.js`,
|
||||
`${gdjsRuntimePath}/CustomRuntimeObjectInstanceContainer.js`,
|
||||
`${gdjsRuntimePath}/events-tools/commontools.js`,
|
||||
`${gdjsRuntimePath}/events-tools/runtimescenetools.js`,
|
||||
`${gdjsRuntimePath}/events-tools/inputtools.js`,
|
||||
`${gdjsRuntimePath}/events-tools/networktools.js`,
|
||||
`${gdjsRuntimePath}/events-tools/objecttools.js`,
|
||||
`${gdjsRuntimePath}/events-tools/cameratools.js`,
|
||||
`${gdjsRuntimePath}/events-tools/soundtools.js`,
|
||||
`${gdjsRuntimePath}/events-tools/storagetools.js`,
|
||||
`${gdjsRuntimePath}/indexeddb.js`,
|
||||
`${gdjsRuntimePath}/events-tools/stringtools.js`,
|
||||
`${gdjsRuntimePath}/events-tools/windowtools.js`,
|
||||
`${gdjsRuntimePath}/debugger-client/abstract-debugger-client.js`,
|
||||
`${gdjsRuntimePath}/debugger-client/hot-reloader.js`,
|
||||
`${gdjsRuntimePath}/gameplay-tests/gameplay-test-runner.js`,
|
||||
`${gdjsRuntimePath}/affinetransformation.js`,
|
||||
|
||||
//Extensions:
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/DraggableBehavior/draggableruntimebehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/AnchorBehavior/anchorruntimebehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/PlatformBehavior/platformerobjectruntimebehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/PlatformBehavior/platformruntimebehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/LinkedObjects/linkedobjects.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Inventory/inventory.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Inventory/inventorytools.js',
|
||||
`${gdjsRuntimePath}/Extensions/DraggableBehavior/draggableruntimebehavior.js`,
|
||||
`${gdjsRuntimePath}/Extensions/AnchorBehavior/anchorruntimebehavior.js`,
|
||||
`${gdjsRuntimePath}/Extensions/PlatformBehavior/platformerobjectruntimebehavior.js`,
|
||||
`${gdjsRuntimePath}/Extensions/PlatformBehavior/platformruntimebehavior.js`,
|
||||
`${gdjsRuntimePath}/Extensions/LinkedObjects/linkedobjects.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Inventory/inventory.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Inventory/inventorytools.js`,
|
||||
{
|
||||
pattern:
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Physics2Behavior/Box2D_v2.3.1_min.wasm.wasm',
|
||||
pattern: `${gdjsRuntimePath}/Extensions/Physics2Behavior/Box2D_v2.3.1_min.wasm.wasm`,
|
||||
watched: true,
|
||||
included: false,
|
||||
served: true,
|
||||
nocache: false,
|
||||
},
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Physics2Behavior/Box2D_v2.3.1_min.wasm.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Physics2Behavior/physics2runtimebehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Physics2Behavior/physics2tools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Leaderboards/leaderboardstools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/PlayerAuthentication/playerauthenticationtools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/PlayerAuthentication/playerauthenticationcomponents.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Multiplayer/messageManager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Multiplayer/multiplayerVariablesManager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Multiplayer/multiplayertools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Multiplayer/multiplayercomponents.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Multiplayer/multiplayerobjectruntimebehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Lighting/lightruntimeobject.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Lighting/lightruntimeobject-pixi-renderer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Lighting/lightobstacleruntimebehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/PathfindingBehavior/PathTools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/PathfindingBehavior/pathfindingobstacleruntimebehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/PathfindingBehavior/pathfindingruntimebehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/NavMeshPathfinding/A_recast-navigation-generators.js',
|
||||
`${gdjsRuntimePath}/Extensions/Physics2Behavior/Box2D_v2.3.1_min.wasm.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Physics2Behavior/physics2runtimebehavior.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Physics2Behavior/physics2tools.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Leaderboards/leaderboardstools.js`,
|
||||
`${gdjsRuntimePath}/Extensions/PlayerAuthentication/playerauthenticationtools.js`,
|
||||
`${gdjsRuntimePath}/Extensions/PlayerAuthentication/playerauthenticationcomponents.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Multiplayer/messageManager.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Multiplayer/multiplayerVariablesManager.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Multiplayer/multiplayertools.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Multiplayer/multiplayercomponents.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Multiplayer/multiplayerobjectruntimebehavior.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Lighting/lightruntimeobject.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Lighting/lightruntimeobject-pixi-renderer.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Lighting/lightobstacleruntimebehavior.js`,
|
||||
`${gdjsRuntimePath}/Extensions/PathfindingBehavior/PathTools.js`,
|
||||
`${gdjsRuntimePath}/Extensions/PathfindingBehavior/pathfindingobstacleruntimebehavior.js`,
|
||||
`${gdjsRuntimePath}/Extensions/PathfindingBehavior/pathfindingruntimebehavior.js`,
|
||||
`${gdjsRuntimePath}/Extensions/NavMeshPathfinding/A_recast-navigation-generators.js`,
|
||||
{
|
||||
pattern:
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/NavMeshPathfinding/recast-navigation.wasm.js',
|
||||
pattern: `${gdjsRuntimePath}/Extensions/NavMeshPathfinding/recast-navigation.wasm.js`,
|
||||
watched: true,
|
||||
included: false,
|
||||
served: true,
|
||||
nocache: false,
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/NavMeshPathfinding/recast-navigation.wasm.wasm',
|
||||
pattern: `${gdjsRuntimePath}/Extensions/NavMeshPathfinding/recast-navigation.wasm.wasm`,
|
||||
watched: true,
|
||||
included: false,
|
||||
served: true,
|
||||
nocache: false,
|
||||
},
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/NavMeshPathfinding/NavMeshObstacleRuntimeBehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/NavMeshPathfinding/NavMeshCharacterRuntimeBehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/PrimitiveDrawing/shapepainterruntimeobject.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/PrimitiveDrawing/shapepainterruntimeobject-pixi-renderer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TextInput/textinputruntimeobject.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TextInput/textinputruntimeobject-pixi-renderer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TextObject/textruntimeobject.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TextObject/textruntimeobject-pixi-renderer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/3D/A_RuntimeObject3D.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/3D/A_RuntimeObject3DRenderer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/3D/Cube3DRuntimeObject.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/3D/Cube3DRuntimeObjectPixiRenderer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/3D/CustomRuntimeObject3D.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/3D/CustomRuntimeObject3DRenderer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/3D/BloomEffect.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/3D/BrightnessAndContrastEffect.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/3D/DepthOfFieldEffect.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/3D/N8AOEffect.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/3D/DirectionalLight.js',
|
||||
`${gdjsRuntimePath}/Extensions/NavMeshPathfinding/NavMeshObstacleRuntimeBehavior.js`,
|
||||
`${gdjsRuntimePath}/Extensions/NavMeshPathfinding/NavMeshCharacterRuntimeBehavior.js`,
|
||||
`${gdjsRuntimePath}/Extensions/PrimitiveDrawing/shapepainterruntimeobject.js`,
|
||||
`${gdjsRuntimePath}/Extensions/PrimitiveDrawing/shapepainterruntimeobject-pixi-renderer.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TextInput/textinputruntimeobject.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TextInput/textinputruntimeobject-pixi-renderer.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TextObject/textruntimeobject.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TextObject/textruntimeobject-pixi-renderer.js`,
|
||||
`${gdjsRuntimePath}/Extensions/3D/A_RuntimeObject3D.js`,
|
||||
`${gdjsRuntimePath}/Extensions/3D/A_RuntimeObject3DRenderer.js`,
|
||||
`${gdjsRuntimePath}/Extensions/3D/Cube3DRuntimeObject.js`,
|
||||
`${gdjsRuntimePath}/Extensions/3D/Cube3DRuntimeObjectPixiRenderer.js`,
|
||||
`${gdjsRuntimePath}/Extensions/3D/CustomRuntimeObject3D.js`,
|
||||
`${gdjsRuntimePath}/Extensions/3D/CustomRuntimeObject3DRenderer.js`,
|
||||
`${gdjsRuntimePath}/Extensions/3D/BloomEffect.js`,
|
||||
`${gdjsRuntimePath}/Extensions/3D/BrightnessAndContrastEffect.js`,
|
||||
`${gdjsRuntimePath}/Extensions/3D/DepthOfFieldEffect.js`,
|
||||
`${gdjsRuntimePath}/Extensions/3D/N8AOEffect.js`,
|
||||
`${gdjsRuntimePath}/Extensions/3D/DirectionalLight.js`,
|
||||
{
|
||||
pattern:
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Physics3DBehavior/jolt-physics.wasm.js',
|
||||
pattern: `${gdjsRuntimePath}/Extensions/Physics3DBehavior/jolt-physics.wasm.js`,
|
||||
watched: true,
|
||||
included: false,
|
||||
served: true,
|
||||
nocache: false,
|
||||
},
|
||||
{
|
||||
pattern:
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Physics3DBehavior/jolt-physics.wasm.wasm',
|
||||
pattern: `${gdjsRuntimePath}/Extensions/Physics3DBehavior/jolt-physics.wasm.wasm`,
|
||||
watched: true,
|
||||
included: false,
|
||||
served: true,
|
||||
nocache: false,
|
||||
},
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Physics3DBehavior/Physics3DTools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Physics3DBehavior/PhysicsCharacter3DRuntimeBehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TopDownMovementBehavior/topdownmovementruntimebehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TweenBehavior/TweenManager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TweenBehavior/tweentools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TweenBehavior/tweenruntimebehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Firebase/A_firebasejs/*.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Firebase/B_firebasetools/*.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Effects/outline-pixi-filter.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Effects/pixi-filters/filter-outline.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Effects/blur-pixi-filter.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Effects/kawase-blur-pixi-filter.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Effects/pixi-filters/filter-kawase-blur.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/tilemapcollisionmaskruntimeobject.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/TileMapRuntimeManager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/TileMapBehavior.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/AbstractTileMapRuntimeObject.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/tilemapruntimeobject-pixi-renderer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/tilemapruntimeobject.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/collision/TileMapCollisionMaskRenderer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/collision/TransformedTileMap.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/helper/TileMapHelper.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/TileMap/pako/dist/pako.min.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Spine/spine-pixi-v7/A_spine-pixi-v7-pre.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Spine/spine-pixi-v7/B_spine-pixi-v7.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Spine/spine-pixi-v7/C_spine-pixi-v7-post.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Spine/spineruntimeobject.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Spine/spineruntimeobject-pixi-renderer.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/Spine/managers/*.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/SaveState/SaveStateTools.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Extensions/SaveState/SaveConfigurationRuntimeBehavior.js',
|
||||
`${gdjsRuntimePath}/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Physics3DBehavior/Physics3DTools.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Physics3DBehavior/PhysicsCharacter3DRuntimeBehavior.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TopDownMovementBehavior/topdownmovementruntimebehavior.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TweenBehavior/TweenManager.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TweenBehavior/tweentools.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TweenBehavior/tweenruntimebehavior.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Firebase/A_firebasejs/*.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Firebase/B_firebasetools/*.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Effects/outline-pixi-filter.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Effects/pixi-filters/filter-outline.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Effects/blur-pixi-filter.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Effects/kawase-blur-pixi-filter.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Effects/pixi-filters/filter-kawase-blur.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TileMap/tilemapcollisionmaskruntimeobject.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TileMap/TileMapRuntimeManager.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TileMap/TileMapBehavior.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TileMap/AbstractTileMapRuntimeObject.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TileMap/tilemapruntimeobject-pixi-renderer.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TileMap/tilemapruntimeobject.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TileMap/collision/TileMapCollisionMaskRenderer.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TileMap/collision/TransformedTileMap.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TileMap/helper/TileMapHelper.js`,
|
||||
`${gdjsRuntimePath}/Extensions/TileMap/pako/dist/pako.min.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Spine/spine-pixi-v7/A_spine-pixi-v7-pre.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Spine/spine-pixi-v7/B_spine-pixi-v7.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Spine/spine-pixi-v7/C_spine-pixi-v7-post.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Spine/spineruntimeobject.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Spine/spineruntimeobject-pixi-renderer.js`,
|
||||
`${gdjsRuntimePath}/Extensions/Spine/managers/*.js`,
|
||||
`${gdjsRuntimePath}/Extensions/SaveState/SaveStateTools.js`,
|
||||
`${gdjsRuntimePath}/Extensions/SaveState/SaveConfigurationRuntimeBehavior.js`,
|
||||
|
||||
// Test extensions:
|
||||
'./GDJS/tests/tests/Extensions/**.js',
|
||||
@@ -252,7 +296,7 @@ module.exports = function (config) {
|
||||
|
||||
// Source maps
|
||||
{
|
||||
pattern: './newIDE/app/resources/GDJS/Runtime/**/*.map',
|
||||
pattern: `${gdjsRuntimePath}/**/*.map`,
|
||||
watched: false,
|
||||
included: false,
|
||||
served: true,
|
||||
@@ -282,8 +326,10 @@ module.exports = function (config) {
|
||||
nocache: false,
|
||||
},
|
||||
|
||||
...testFiles,
|
||||
...(config.enableBenchmarks ? benchmarkFiles : []),
|
||||
...(config.benchmarksOnly ? [] : testFiles),
|
||||
...(config.enableBenchmarks || config.benchmarksOnly
|
||||
? benchmarkFiles
|
||||
: []),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"test:chromeHeadlessNoSandbox": "karma start --browsers ChromeHeadlessNoSandbox --single-run",
|
||||
"test-benchmark": "karma start --browsers ChromeHeadless --single-run --enableBenchmarks",
|
||||
"test-benchmark:watch": "karma start --browsers ChromeHeadless --enableBenchmarks",
|
||||
"benchmark:compare": "node compare-benchmarks.js",
|
||||
"test:firefox": "karma start --browsers Firefox --single-run",
|
||||
"test:firefox:watch": "karma start --browsers Firefox",
|
||||
"test:chrome": "karma start --browsers Chrome --single-run",
|
||||
|
||||
@@ -978,6 +978,62 @@ describe('gdjs.gameplayTests', () => {
|
||||
expect(result.status).to.be('passed');
|
||||
});
|
||||
|
||||
it('benchmarks frames and reports the measures in the result', async () => {
|
||||
const runtimeGame = makeRuntimeGame();
|
||||
const result = await runTestScript(
|
||||
runtimeGame,
|
||||
`
|
||||
await harness.goToScene('Scene 1');
|
||||
harness.spawn('MyObject', 10, 20);
|
||||
const benchmark = await harness.benchmark('Test benchmark', {
|
||||
warmupFrames: 2,
|
||||
frames: 5,
|
||||
});
|
||||
harness.assert(benchmark.frameTimesMs.length === 5, '5 frames are measured');
|
||||
harness.assert(
|
||||
benchmark.medianFrameTimeMs >= 0,
|
||||
'The median frame time is returned'
|
||||
);
|
||||
harness.assert(benchmark.objectsCount === 1, 'The objects are counted');
|
||||
harness.assert(
|
||||
benchmark.drawCallsPerFrame === null,
|
||||
'No draw calls are counted without renderer'
|
||||
);
|
||||
`
|
||||
);
|
||||
|
||||
expect(result.status).to.be('passed');
|
||||
expect(result.framesExecuted).to.be(1 + 2 + 5);
|
||||
expect(result.benchmarks.length).to.be(1);
|
||||
expect(result.benchmarks[0].name).to.be('Test benchmark');
|
||||
expect(result.benchmarks[0].stateChecksum).to.be.a('string');
|
||||
});
|
||||
|
||||
it('seeds Math.random until the end of the test', async () => {
|
||||
const originalMathRandom = Math.random;
|
||||
const runTestScriptPickingRandomNumbers = () =>
|
||||
runTestScript(
|
||||
makeRuntimeGame(),
|
||||
`
|
||||
harness.setRandomSeed(42);
|
||||
await harness.goToScene('Scene 1');
|
||||
console.log('Random numbers: ' + [Math.random(), Math.random()]);
|
||||
`
|
||||
);
|
||||
/** @param {gdjs.gameplayTests.GameplayTestResult} result */
|
||||
const getRandomNumbersLog = (result) =>
|
||||
result.consoleLogs.find((log) =>
|
||||
log.message.startsWith('Random numbers: ')
|
||||
);
|
||||
const result1 = await runTestScriptPickingRandomNumbers();
|
||||
const result2 = await runTestScriptPickingRandomNumbers();
|
||||
|
||||
expect(result1.status).to.be('passed');
|
||||
expect(getRandomNumbersLog(result1)).to.be.ok();
|
||||
expect(getRandomNumbersLog(result1)).to.eql(getRandomNumbersLog(result2));
|
||||
expect(Math.random).to.be(originalMathRandom);
|
||||
});
|
||||
|
||||
it('reports an aim result object with the mouse responsiveness', async () => {
|
||||
const runtimeGame = makeRuntimeGame();
|
||||
const result = await runTestScript(
|
||||
|
||||
Reference in New Issue
Block a user