Skip to content

♻️ use inline plugin #46

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

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8cc11d6
🚧 inline: improve inlining conditionals
krispya Jan 19, 2025
dff9466
➕ n-body: swap to publish
krispya Jan 19, 2025
9d17a1d
🐛 ♻️ inline: streamline code and fix bugs
krispya Jan 19, 2025
018fa30
➕ inline: fix dev deps
krispya Jan 19, 2025
9ce16f4
♻️ core: move some strings around
krispya Jan 20, 2025
a9d689b
🐛 inline: now properly does 2 passes
krispya Jan 20, 2025
7f319f2
♻️ core: inline more functions
krispya Jan 20, 2025
5762a18
🐛 inline: properly support control flow
krispya Jan 23, 2025
75e377d
♻️ core: for inlining tests
krispya Jan 23, 2025
1e715c2
🐛 fix dep imports, support per-call inlining
krispya Jan 24, 2025
b0d519a
📝 inline: notes for me
krispya Jan 24, 2025
a7d9967
♻️ core: inline testing
krispya Jan 24, 2025
4b451a7
✨ inline: remove duplicate property accesses
krispya Jan 29, 2025
045c5b0
♻️ core: more inlinining tests
krispya Jan 29, 2025
0677c64
⚰️ inilne: remove unused code
krispya Jan 31, 2025
ed807d6
🐛 inline: only dedup consts
krispya Jan 31, 2025
0b1056c
🔊 ⚡️ ♻️ inilne: improved caching, added stats, log stats
krispya Jan 31, 2025
dada3e4
🔊 inline: only print the log once if results are cached
krispya Jan 31, 2025
ac70e2b
♻️ ✨ 🐛 inline: only dedups pure functions
krispya Jan 31, 2025
d353693
🐛 inline: bail on deduping if we are in an if statement
krispya Jan 31, 2025
f41ca39
⚡️ core: optimizie shallow equal
krispya Jan 31, 2025
83e1847
♻️ core: turn on more inlining
krispya Jan 31, 2025
a2ec482
♻️ core: simplify
krispya Feb 3, 2025
211f35b
🔀 Merge branch 'main' into exp/inline-plugin
krispya Feb 3, 2025
957f487
🐛 fix the merge chaos
krispya Feb 3, 2025
dc754c4
♻️ 🐛 ✨ inline: now uses correct babel APIs
krispya Feb 5, 2025
aa46875
♻️ core: add more inlining
krispya Feb 5, 2025
ac0963a
🚚 inline: correct name babel -> esbuild
krispya Feb 5, 2025
4c25ac5
🔥 inline: move the plugin to an external repo
krispya Feb 5, 2025
019e51f
🔧 make sure we are using the dev package
krispya Feb 5, 2025
a203f33
🔀 Merge branch 'main' into exp/inline-plugin
krispya Apr 26, 2025
df573c7
🔀 finish merging
krispya Apr 26, 2025
6ab7232
⚰️ core: cleanup
krispya Apr 27, 2025
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
54 changes: 16 additions & 38 deletions packages/core/src/entity/entity-methods-patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,90 +3,68 @@
// and the convenience of using methods. Type guards are used to ensure
// that the methods are only called on entities.

import { addTrait, getTrait, hasTrait, removeTrait, setTrait } from '../trait/trait';
import { ConfigurableTrait, Trait } from '../trait/types';
import { $internal } from '../common';
import { setChanged } from '../query/modifiers/changed';
import { getRelationTargets } from '../relation/relation';
import { Relation } from '../relation/types';
import { universe } from '../universe/universe';
import { $internal } from '../common';
import { destroyEntity } from './entity';
import { addTrait, getTrait, hasTrait, removeTrait, setTrait } from '../trait/trait';
import { ConfigurableTrait, Trait } from '../trait/types';
import { destroyEntity, getEntityWorld } from './entity';
import { Entity } from './types';
import { ENTITY_ID_MASK, WORLD_ID_SHIFT } from './utils/pack-entity';
import { getEntityId } from './utils/pack-entity';
import { isEntityAlive } from './utils/entity-index';

// @ts-expect-error
Number.prototype.add = function (this: Entity, ...traits: ConfigurableTrait[]) {
const worldId = this >>> WORLD_ID_SHIFT;
const world = universe.worlds[worldId]!.deref()!;
return addTrait(world, this, ...traits);
return addTrait(getEntityWorld(this), this, ...traits);
};

// @ts-expect-error
Number.prototype.remove = function (this: Entity, ...traits: Trait[]) {
const worldId = this >>> WORLD_ID_SHIFT;
const world = universe.worlds[worldId]!.deref()!;
return removeTrait(world, this, ...traits);
return removeTrait(getEntityWorld(this), this, ...traits);
};

// @ts-expect-error
Number.prototype.has = function (this: Entity, trait: Trait) {
const worldId = this >>> WORLD_ID_SHIFT;
const world = universe.worlds[worldId]!.deref()!;
return hasTrait(world, this, trait);
return hasTrait(getEntityWorld(this), this, trait);
};

// @ts-expect-error
Number.prototype.destroy = function (this: Entity) {
const worldId = this >>> WORLD_ID_SHIFT;
const world = universe.worlds[worldId]!.deref()!;
return destroyEntity(world, this);
return destroyEntity(getEntityWorld(this), this);
};

// @ts-expect-error
Number.prototype.changed = function (this: Entity, trait: Trait) {
const worldId = this >>> WORLD_ID_SHIFT;
const world = universe.worlds[worldId]!.deref()!;
return setChanged(world, this, trait);
return setChanged(getEntityWorld(this), this, trait);
};

// @ts-expect-error
Number.prototype.get = function (this: Entity, trait: Trait) {
const worldId = this >>> WORLD_ID_SHIFT;
const world = universe.worlds[worldId]!.deref()!;
return getTrait(world, this, trait);
return getTrait(getEntityWorld(this), this, trait);
};

// @ts-expect-error
Number.prototype.set = function (this: Entity, trait: Trait, value: any, triggerChanged = true) {
const worldId = this >>> WORLD_ID_SHIFT;
const world = universe.worlds[worldId]!.deref()!;
setTrait(world, this, trait, value, triggerChanged);
setTrait(getEntityWorld(this), this, trait, value, triggerChanged);
};

//@ts-expect-error
Number.prototype.targetsFor = function (this: Entity, relation: Relation<any>) {
const worldId = this >>> WORLD_ID_SHIFT;
const world = universe.worlds[worldId]!.deref()!;
return getRelationTargets(world, relation, this);
return getRelationTargets(getEntityWorld(this), relation, this);
};

//@ts-expect-error
Number.prototype.targetFor = function (this: Entity, relation: Relation<any>) {
const worldId = this >>> WORLD_ID_SHIFT;
const world = universe.worlds[worldId]!.deref()!;
return getRelationTargets(world, relation, this)[0];
return getRelationTargets(getEntityWorld(this), relation, this)[0];
};

//@ts-expect-error
Number.prototype.id = function (this: Entity) {
const id = this & ENTITY_ID_MASK;
return id;
return getEntityId(this);
};

//@ts-expect-error
Number.prototype.isAlive = function (this: Entity) {
const worldId = this >>> WORLD_ID_SHIFT;
const world = universe.worlds[worldId]!.deref()!;
return isEntityAlive(world[$internal].entityIndex, this);
return isEntityAlive(getEntityWorld(this)[$internal].entityIndex, this);
};
12 changes: 9 additions & 3 deletions packages/core/src/entity/entity.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { $internal } from '../common';
import { Pair, Wildcard } from '../relation/relation';
import { addTrait, removeTrait } from '../trait/trait';
import { ConfigurableTrait } from '../trait/types';
import { Pair, Wildcard } from '../relation/relation';
import { $internal } from '../common';
import { World } from '../world/world';
import { Entity } from './types';
import { allocateEntity, releaseEntity } from './utils/entity-index';
import { getEntityId } from './utils/pack-entity';
import { universe } from '../universe/universe';
import { getEntityId, getEntityWorldId } from './utils/pack-entity';

// Ensure entity methods are patched.
import './entity-methods-patch';
Expand Down Expand Up @@ -106,3 +107,8 @@ export function destroyEntity(world: World, entity: Entity) {
}
}
}

/* @inline @pure */ export function getEntityWorld(entity: Entity) {
const worldId = getEntityWorldId(entity);
return universe.worlds[worldId]!.deref()!;
}
7 changes: 3 additions & 4 deletions packages/core/src/entity/utils/pack-entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ export function unpackEntity(entity: Entity) {
};
}

export const getEntityId = (entity: Entity) => entity & ENTITY_ID_MASK;
export const getEntityWorldId = (entity: Entity) => entity >>> WORLD_ID_SHIFT;

export const getEntityAndWorldId = (entity: Entity): [number, number] => [
export const getEntityId = /* @inline @pure */ (entity: Entity) => entity & ENTITY_ID_MASK;
export const getEntityWorldId = /* @inline @pure */ (entity: Entity) => entity >>> WORLD_ID_SHIFT;
export const getEntityAndWorldId = /* @pure */ (entity: Entity): [number, number] => [
entity & ENTITY_ID_MASK,
entity >>> WORLD_ID_SHIFT,
];
Expand Down
88 changes: 52 additions & 36 deletions packages/core/src/query/query-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,29 +47,13 @@ export function createQueryResult<T extends QueryParameter[]>(
const trackedIndices: number[] = [];
const untrackedIndices: number[] = [];

// Get the traits that are being tracked for changes.
for (let i = 0; i < traits.length; i++) {
const trait = traits[i];
const hasTracked = world[$internal].trackedTraits.has(trait);
const hasChanged = query.hasChangedModifiers && query.changedTraits.has(trait);

if (hasTracked || hasChanged) trackedIndices.push(i);
else untrackedIndices.push(i);
}
getTrackedTraits(traits, world, query, trackedIndices, untrackedIndices);

for (let i = 0; i < entities.length; i++) {
const entity = entities[i];
const eid = getEntityId(entity);

// Create a snapshot for each trait in the order they appear in the query params.
for (let j = 0; j < traits.length; j++) {
const trait = traits[j];
const ctx = trait[$internal];
const value = ctx.get(eid, stores[j]);
state[j] = value;
atomicSnapshots[j] = ctx.type === 'aos' ? { ...value } : null;
}

createSnapshotsWithAtomic(eid, traits, stores, state, atomicSnapshots);
callback(state as any, entity, i);

// Skip if the entity has been destroyed.
Expand Down Expand Up @@ -120,15 +104,7 @@ export function createQueryResult<T extends QueryParameter[]>(
const entity = entities[i];
const eid = getEntityId(entity);

// Create a snapshot for each trait in the order they appear in the query params.
for (let j = 0; j < traits.length; j++) {
const trait = traits[j];
const ctx = trait[$internal];
const value = ctx.get(eid, stores[j]);
state[j] = value;
atomicSnapshots[j] = ctx.type === 'aos' ? { ...value } : null;
}

createSnapshotsWithAtomic(eid, traits, stores, state, atomicSnapshots);
callback(state as any, entity, i);

// Skip if the entity has been destroyed.
Expand Down Expand Up @@ -164,14 +140,7 @@ export function createQueryResult<T extends QueryParameter[]>(
for (let i = 0; i < entities.length; i++) {
const entity = entities[i];
const eid = getEntityId(entity);

// Create a snapshot for each trait in the order they appear in the query params.
for (let j = 0; j < traits.length; j++) {
const trait = traits[j];
const ctx = trait[$internal];
state[j] = ctx.get(eid, stores[j]);
}

createSnapshots(eid, traits, stores, state);
callback(state as any, entity, i);

// Skip if the entity has been destroyed.
Expand Down Expand Up @@ -212,7 +181,54 @@ export function createQueryResult<T extends QueryParameter[]>(
return results;
}

function getQueryStores<T extends QueryParameter[]>(
/* @inline */ function getTrackedTraits(
traits: Trait[],
world: World,
query: Query,
trackedIndices: number[],
untrackedIndices: number[]
) {
for (let i = 0; i < traits.length; i++) {
const trait = traits[i];
const hasTracked = world[$internal].trackedTraits.has(trait);
const hasChanged = query.hasChangedModifiers && query.changedTraits.has(trait);

if (hasTracked || hasChanged) trackedIndices.push(i);
else untrackedIndices.push(i);
}
}

/* @inline */ function createSnapshots(
entityId: number,
traits: Trait[],
stores: Store<any>[],
state: any[]
) {
for (let i = 0; i < traits.length; i++) {
const trait = traits[i];
const ctx = trait[$internal];
const value = ctx.get(entityId, stores[i]);
state[i] = value;
}
}

/* @inline */ function createSnapshotsWithAtomic(
entityId: number,
traits: Trait[],
stores: Store<any>[],
state: any[],
atomicSnapshots: any[]
) {
for (let j = 0; j < traits.length; j++) {
const trait = traits[j];
const ctx = trait[$internal];
const value = ctx.get(entityId, stores[j]);
state[j] = value;
atomicSnapshots[j] = ctx.type === 'aos' ? { ...value } : null;
}
}

/* @inline */ function getQueryStores<T extends QueryParameter[]>(
params: T,
traits: Trait[],
stores: Store<any>[],
Expand Down
32 changes: 11 additions & 21 deletions packages/core/src/trait/trait.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { $internal } from '../common';
import { Entity } from '../entity/types';
import { ENTITY_ID_MASK, getEntityId } from '../entity/utils/pack-entity';
import { getEntityId } from '../entity/utils/pack-entity';
import { setChanged } from '../query/modifiers/changed';
import { getRelationTargets, Pair, Wildcard } from '../relation/relation';
import { incrementWorldBitflag } from '../world/utils/increment-world-bit-flag';
Expand Down Expand Up @@ -224,7 +224,7 @@ export function removeTrait(world: World, entity: Entity, ...traits: Trait[]) {
}
}

export function hasTrait(world: World, entity: Entity, trait: Trait): boolean {
export /* @inline @pure */ function hasTrait(world: World, entity: Entity, trait: Trait): boolean {
const ctx = world[$internal];
const data = ctx.traitData.get(trait);
if (!data) return false;
Expand All @@ -236,12 +236,13 @@ export function hasTrait(world: World, entity: Entity, trait: Trait): boolean {
return (mask & bitflag) === bitflag;
}

export function getStore<C extends Trait = Trait>(world: World, trait: C): ExtractStore<C> {
export /* @inline @pure */ function getStore<C extends Trait = Trait>(
world: World,
trait: C
): ExtractStore<C> {
const ctx = world[$internal];
const data = ctx.traitData.get(trait)!;
const store = data.store as ExtractStore<C>;

return store;
return data.store as ExtractStore<C>;
}

export function setTrait(
Expand All @@ -252,8 +253,8 @@ export function setTrait(
triggerChanged = true
) {
const ctx = trait[$internal];
const index = entity & ENTITY_ID_MASK;
const store = getStore(world, trait);
const index = getEntityId(entity);

// A short circuit is more performance than an if statement which creates a new code statement.
value instanceof Function && (value = value(ctx.get(index, store)));
Expand All @@ -263,21 +264,10 @@ export function setTrait(
}

export function getTrait(world: World, entity: Entity, trait: Trait) {
const worldCtx = world[$internal];
const data = worldCtx.traitData.get(trait);

// If the trait does not exist on the world return undefined.
if (!data) return undefined;

// Get entity index/id.
const index = getEntityId(entity);

// If the entity does not have the trait return undefined.
const mask = worldCtx.entityMasks[data.generationId][index];
if ((mask & data.bitflag) !== data.bitflag) return undefined;
const result = hasTrait(world, entity, trait);
if (!result) return undefined;

// Return a snapshot of the trait state.
const traitCtx = trait[$internal];
const store = getStore(world, trait);
return traitCtx.get(index, store);
return traitCtx.get(getEntityId(entity), store);
}
40 changes: 18 additions & 22 deletions packages/core/src/utils/shallow-equal.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
export function shallowEqual(obj1: any, obj2: any): boolean {
if (obj1 === obj2) {
return true;
}
// This shallow equal looks insane because it is optimized to use short circuiting
// and the least amount of evaluations possible.

if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
return false;
}

const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);

if (keys1.length !== keys2.length) {
return false;
}

for (const key of keys1) {
if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) {
return false;
}
}

return true;
export function /* @inline @pure */ shallowEqual(obj1: any, obj2: any): boolean {
return (
obj1 === obj2 ||
(typeof obj1 === 'object' &&
obj1 !== null &&
typeof obj2 === 'object' &&
obj2 !== null &&
(() => {
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
return (
keys1.length === keys2.length &&
keys1.every((key) => obj2.hasOwnProperty(key) && obj1[key] === obj2[key])
);
})())
);
}
1 change: 1 addition & 0 deletions packages/publish/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"devDependencies": {
"@koota/core": "workspace:*",
"@koota/react": "workspace:*",
"esbuild-plugin-inline-functions": "^0.1.0",
"react": ">=18.0.0",
"react-dom": ">=18.0.0",
"tsconfig": "workspace:*",
Expand Down
2 changes: 2 additions & 0 deletions packages/publish/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineConfig } from 'tsup';
import { inlineFunctionsPlugin } from 'esbuild-plugin-inline-functions';

export default defineConfig({
entry: ['src/index.ts', 'src/react.ts'],
Expand All @@ -7,4 +8,5 @@ export default defineConfig({
resolve: true,
},
clean: true,
esbuildPlugins: [inlineFunctionsPlugin()],
});
Loading