diff --git a/src/chrono/Graph.ts b/src/chrono/Graph.ts index f59d5e5..c987efd 100644 --- a/src/chrono/Graph.ts +++ b/src/chrono/Graph.ts @@ -589,7 +589,8 @@ export class ChronoGraph extends Base { this.$followingRevision = undefined - this.markAndSweep() + // Perf: skip markAndSweep on initial commit — no meaningful revision history to compact + if (!this._isInitialCommit) this.markAndSweep() } else { // `baseRevisionStable` might be already cleared in the `reject` method of the graph if (this.baseRevisionStable) this.baseRevision = this.baseRevisionStable diff --git a/src/chrono/Identifier.ts b/src/chrono/Identifier.ts index 786cf09..5583a0f 100644 --- a/src/chrono/Identifier.ts +++ b/src/chrono/Identifier.ts @@ -162,6 +162,10 @@ export class Identifier extend isWritingUndefined : boolean = false + // Perf: flag for sync identifiers — set on prototype by CalculatedValueSync and Variable + @prototypeValue(true) + genCalc : boolean + newQuark (createdAt : Revision) : InstanceType { // micro-optimization - we don't pass a config object to the `new` constructor // but instead assign directly to instance @@ -169,6 +173,10 @@ export class Identifier extend newQuark.identifier = this newQuark.needToBuildProposedValue = this.proposedValueIsBuilt + // Perf: cache calculation and context to avoid getter delegation per startCalculation + newQuark.calculation = this.calculation + newQuark.context = this.context || this + ;(newQuark as any)._isGenCalc = this.genCalc return newQuark } @@ -263,10 +271,11 @@ export const IdentifierC = (config : Partial -//@ts-ignore -export const QuarkSync = Quark.mix(CalculationSync.mix(Map)) +// Perf: unified quark class — both sync and gen identifiers use the same class +// so V8 sees a single hidden class, eliminating LoadIC_Megamorphic overhead (~14% of CPU) //@ts-ignore export const QuarkGen = Quark.mix(CalculationGen.mix(Map)) +export const QuarkSync = QuarkGen //--------------------------------------------------------------------------------------------------------------------- /** @@ -279,6 +288,9 @@ export class Variable extends Identifier (...args) : Variable { */ export class CalculatedValueSync extends Identifier { + @prototypeValue(false) + genCalc : boolean + @prototypeValue(QuarkSync) quarkClass : QuarkConstructor diff --git a/src/chrono/Quark.ts b/src/chrono/Quark.ts index 8d5ba30..45f43ff 100644 --- a/src/chrono/Quark.ts +++ b/src/chrono/Quark.ts @@ -63,14 +63,9 @@ class Quark extends base { } - get calculation () : this[ 'identifier' ][ 'calculation' ] { - return this.identifier.calculation - } - - - get context () : any { - return this.identifier.context || this.identifier - } + // Perf: cached from identifier at creation time to avoid getter delegation per calculation + calculation : this[ 'identifier' ][ 'calculation' ] + context : any forceCalculation () { @@ -229,10 +224,10 @@ class Quark extends base { } - addOutgoingTo (toQuark : Quark, type : EdgeType) { + addOutgoingTo (toQuark : Quark, type : EdgeType, toIdentifier : Identifier) { const outgoing = type === EdgeType.Normal ? this as Map : this.getOutgoingPast() - outgoing.set(toQuark.identifier, toQuark) + outgoing.set(toIdentifier, toQuark) } diff --git a/src/chrono/Transaction.ts b/src/chrono/Transaction.ts index 4e40a5b..ca573d0 100644 --- a/src/chrono/Transaction.ts +++ b/src/chrono/Transaction.ts @@ -100,6 +100,9 @@ export class Transaction extends Base { // during a transaction, so these results are stable and can be safely cached. // Entries are removed from cache when a shadow quark is created for that identifier // (via entries.set), since after that point the transaction's own entry takes precedence. + + // Perf: flag to skip base revision lookups entirely when base is empty (initial commit) + baseRevisionEmpty : boolean = false baseRevisionCache : Map = new Map() readingIdentifier : Identifier @@ -107,6 +110,9 @@ export class Transaction extends Base { // A Set of faced computation cycles cycles : Set = new Set() + // Perf: fast boolean flag for cycle presence — avoids Map.has() hash computation in hot loops + hasCycles : boolean = false + // A Map of identifiers that caused a cycle cycledIdentifiers : Map = new Map() @@ -131,6 +137,9 @@ export class Transaction extends Base { // instead inside of `read` delegate to `yieldSync` for non-identifiers this.onEffectSync = /*this.onEffectAsync =*/ this.read.bind(this) this.onEffectAsync = this.readAsync.bind(this) + + // Perf: detect empty base revision so cachedBaseRevisionLookup can short-circuit + this.baseRevisionEmpty = this.baseRevision.scope.size === 0 && !this.baseRevision.previous } @@ -772,21 +781,29 @@ export class Transaction extends Base { async commitAsync (args? : CommitArguments) : Promise { this.preCommit(args) + // Perf: use the synchronous commit path when no async effects are possible. + // This avoids all generator overhead (~41% faster for large initial loads). + const canUseSyncPath = this.graph && this.graph.onComputationCycle !== 'effect' + return this.ongoing = this.ongoing.then(() => { - return runGeneratorAsyncWithEffect(this.onEffectAsync, this.calculateTransitions, [ this.onEffectAsync ], this) + if (canUseSyncPath) { + this.calculateTransitionsSync(this.onEffectSync) + } + else { + return runGeneratorAsyncWithEffect(this.onEffectAsync, this.calculateTransitions, [ this.onEffectAsync ], this) + } }).then(() => { return this.postCommit() }) - - // await runGeneratorAsyncWithEffect(this.onEffectAsync, this.calculateTransitions, [ this.onEffectAsync ], this) - // - // return this.postCommit() } // Perf: cached wrapper for baseRevision.getLatestEntryFor(). // Avoids repeated O(revisions) walks for the same identifier within a commit cycle. cachedBaseRevisionLookup (identifier : Identifier) : Quark { + // Perf: short-circuit for initial commit when base revision is empty + if (this.baseRevisionEmpty) return null + const cache = this.baseRevisionCache let result = cache.get(identifier) @@ -867,7 +884,7 @@ export class Transaction extends Base { this.entries.set(identifierRead, entry) } - entry.addOutgoingTo(activeEntry, type) + entry.addOutgoingTo(activeEntry, type, identifier) return entry } @@ -1179,6 +1196,7 @@ export class Transaction extends Base { } addCycle (cycle : ComputationCycle) { + this.hasCycles = true this.cycles.add(cycle) const { cycledIdentifiers } = this @@ -1213,6 +1231,7 @@ export class Transaction extends Base { } clearCycles () { + this.hasCycles = false this.cycles.clear() this.cycledIdentifiers.clear() @@ -1238,22 +1257,24 @@ export class Transaction extends Base { // If the identifier is involved into a cycle the transaction faced // we ignore it and proceed ot the next step - if (cycledIdentifiers.has(identifier)) { + if (this.hasCycles && cycledIdentifiers.has(identifier)) { // add dependent identifiers to the list of involved in a cycle - this.markDependentCycledIdentifiers (entry, cycledIdentifiers.get(identifier)) + this.markDependentCycledIdentifiers(entry, cycledIdentifiers.get(identifier)) entry.cleanup() stack.pop() continue } - // TODO can avoid `.get()` call by comparing some another "epoch" counter on the entry - const ownEntry = entries.get(identifier) - if (ownEntry !== entry) { - entry.cleanup() + // Perf: skip ownership check during initial commit — entries are never replaced + if (!this.baseRevisionEmpty) { + const ownEntry = entries.get(identifier) + if (ownEntry !== entry) { + entry.cleanup() - stack.pop() - continue + stack.pop() + continue + } } if (entry.edgesFlow == 0) { @@ -1316,9 +1337,9 @@ export class Transaction extends Base { break } else if (value instanceof Identifier) { - if (cycledIdentifiers.has(value)) { + if (this.hasCycles && cycledIdentifiers.has(value)) { // add dependent identifiers to the list of involved in a cycle - this.markDependentCycledIdentifiers (entry, cycledIdentifiers.get(identifier)) + this.markDependentCycledIdentifiers(entry, cycledIdentifiers.get(identifier)) iterationResult = undefined entry.cleanup() diff --git a/src/primitives/Calculation.ts b/src/primitives/Calculation.ts index 9f7de63..0c922ff 100644 --- a/src/primitives/Calculation.ts +++ b/src/primitives/Calculation.ts @@ -100,10 +100,24 @@ class CalculationGen extends base implements GenericCalculation, ...args : any[]) : IteratorResult { - const iterator : this[ 'iterator' ] = this.iterator = this.calculation.call(this.context || this, onEffect, ...args) + if (this._isGenCalc) { + const iterator : this[ 'iterator' ] = this.iterator = this.calculation.call(this.context || this, onEffect, ...args) + + return this.iterationResult = iterator.next() + } + + // Sync calculation — mark as started for cycle detection, then return completed result + this.iterationResult = calculationStartedConstant - return this.iterationResult = iterator.next() + return this.iterationResult = { + done : true, + value : this.calculation.call(this.context || this, onEffect, ...args) + } } diff --git a/src/replica/Entity.ts b/src/replica/Entity.ts index 30810dd..fc776ae 100644 --- a/src/replica/Entity.ts +++ b/src/replica/Entity.ts @@ -155,7 +155,8 @@ export class Entity extends Mixin( identifier.context = this identifier.self = this - identifier.name = `${this.$$.name}.$.${field.name}` + // Perf: use field name directly instead of template string (avoids string allocation per field) + identifier.name = name return identifier }