Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 242 additions & 0 deletions Benchmarks/AdaECSBenchmarks/AdaECSBenchmarks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import AdaECS
import Benchmark

// MARK: - Components

@Component
private struct Position {
var x: Float
var y: Float
}

@Component
private struct Velocity {
var x: Float
var y: Float
}

@Component
private struct CompA {
var value: Float
}

@Component
private struct CompB {
var value: Float
}

@Component
private struct CompC {
var value: Float
}

@Component
private struct FragData {
var value: Float
}

@Component
private struct Matrix4x4 {
var m00: Float, m01: Float, m02: Float, m03: Float
var m10: Float, m11: Float, m12: Float, m13: Float
var m20: Float, m21: Float, m22: Float, m23: Float
var m30: Float, m31: Float, m32: Float, m33: Float
}

// MARK: - Constants

private enum BenchConstants {
static let spawnEntities = 100_000
static let simpleIterEntities = 100_000
static let fragmentedEntitiesPerType = 33_334
static let heavyComputeEntities = 1_000
static let heavyComputeIterations = 100
static let addRemoveEntities = 100_000
}

// MARK: - Setup Helpers

private func makeSimpleIterWorld(entityCount: Int) -> (World, Query<Ref<Position>, Velocity>) {
let world = World()
for i in 0..<entityCount {
world.spawn {
Position(x: Float(i), y: Float(i))
Velocity(x: 1, y: -1)
}
}

let query = Query<Ref<Position>, Velocity>()
query.update(from: world)

return (world, query)
}

private func makeFragmentedIterWorld(entitiesPerType: Int) -> (World, Query<Ref<FragData>>) {
let world = World()

for i in 0..<entitiesPerType {
world.spawn {
CompA(value: Float(i))
FragData(value: Float(i))
}
}
for i in 0..<entitiesPerType {
world.spawn {
CompB(value: Float(i))
FragData(value: Float(i))
}
}
for i in 0..<entitiesPerType {
world.spawn {
CompC(value: Float(i))
FragData(value: Float(i))
}
}

let query = Query<Ref<FragData>>()
query.update(from: world)

return (world, query)
}

private func makeHeavyComputeWorld(entityCount: Int) -> (World, Query<Ref<Matrix4x4>>) {
let world = World()
for _ in 0..<entityCount {
world.spawn {
Matrix4x4(
m00: 1, m01: 0, m02: 0, m03: 0,
m10: 0, m11: 1, m12: 0, m13: 0,
m20: 0, m21: 0, m22: 1, m23: 0,
m30: 0, m31: 0, m32: 0, m33: 1
)
}
}

let query = Query<Ref<Matrix4x4>>()
query.update(from: world)

return (world, query)
}

private func makeAddRemoveWorld(entityCount: Int) -> (World, [Entity.ID]) {
let world = World()
var ids: [Entity.ID] = []
ids.reserveCapacity(entityCount)

for i in 0..<entityCount {
let entity = world.spawn {
Position(x: Float(i), y: Float(i))
}
ids.append(entity.id)
}

return (world, ids)
}

// MARK: - Benchmarks

let benchmarks: @Sendable () -> Void = {
Benchmark("AdaECS.Spawn") { benchmark in
// #region agent log
DebugBenchmarkLog.benchmarkPhase(name: "Spawn", phase: "start")
// #endregion
for _ in benchmark.scaledIterations {
let world = World()
for _ in 0..<BenchConstants.spawnEntities {
world.spawn()
}
blackHole(world)
}
// #region agent log
DebugBenchmarkLog.benchmarkPhase(name: "Spawn", phase: "end")
// #endregion
}

Benchmark(
"AdaECS.SimpleIter",
closure: { benchmark, state in
// #region agent log
DebugBenchmarkLog.benchmarkPhase(name: "SimpleIter", phase: "start")
// #endregion
let (world, query) = state
for _ in benchmark.scaledIterations {
query.forEach { position, velocity in
position.x += velocity.x
position.y += velocity.y
}
}
blackHole(world)
// #region agent log
DebugBenchmarkLog.benchmarkPhase(name: "SimpleIter", phase: "end")
// #endregion
},
setup: {
makeSimpleIterWorld(entityCount: BenchConstants.simpleIterEntities)
}
)

Benchmark(
"AdaECS.FragmentedIter",
closure: { benchmark, state in
// #region agent log
DebugBenchmarkLog.benchmarkPhase(name: "FragmentedIter", phase: "start")
// #endregion
let (world, query) = state
for _ in benchmark.scaledIterations {
for data in query {
data.value *= 2.0
}
}
blackHole(world)
// #region agent log
DebugBenchmarkLog.benchmarkPhase(name: "FragmentedIter", phase: "end")
// #endregion
},
setup: {
makeFragmentedIterWorld(entitiesPerType: BenchConstants.fragmentedEntitiesPerType)
}
)

Benchmark(
"AdaECS.HeavyCompute",
closure: { benchmark, state in
let (world, query) = state
for _ in benchmark.scaledIterations {
for transform in query {
for _ in 0..<BenchConstants.heavyComputeIterations {
transform.m00 += 1.0
}
}
}
blackHole(world)
},
setup: {
makeHeavyComputeWorld(entityCount: BenchConstants.heavyComputeEntities)
}
)

Benchmark(
"AdaECS.AddRemove",
closure: { benchmark, state in
// #region agent log
DebugBenchmarkLog.benchmarkPhase(name: "AddRemove", phase: "start")
// #endregion
let (world, ids) = state
for _ in benchmark.scaledIterations {
for id in ids {
world.insert(Velocity(x: 1, y: -1), for: id)
}
for id in ids {
world.remove(Velocity.self, from: id)
}
}
blackHole(world)
// #region agent log
DebugBenchmarkLog.benchmarkPhase(name: "AddRemove", phase: "end")
// #endregion
},
setup: {
makeAddRemoveWorld(entityCount: BenchConstants.addRemoveEntities)
}
)
}
23 changes: 20 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,22 @@ targets += [
)
]

// MARK: - Benchmarks

targets += [
.executableTarget(
name: "AdaECSBenchmarks",
dependencies: [
.product(name: "Benchmark", package: "package-benchmark"),
"AdaECS"
],
path: "Benchmarks/AdaECSBenchmarks",
plugins: [
.plugin(name: "BenchmarkPlugin", package: "package-benchmark")
]
)
]

// MARK: - Tests

targets += [
Expand Down Expand Up @@ -903,6 +919,7 @@ package.dependencies += [
.package(url: "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/apple/swift-atomics", from: "1.3.0"),
.package(url: "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/the-swift-collective/zlib.git", from: "1.3.2"),
.package(url: "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/swiftlang/swift-subprocess.git", branch: "0.2.1"),
.package(url: "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/ordo-one/package-benchmark", from: "1.29.0"),
// TODO: SpectralDragon packages should move to AdaEngine
.package(url: "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/SpectralDragon/Yams.git", revision: "fb676da"),
.package(
Expand Down Expand Up @@ -985,12 +1002,12 @@ private extension Target {
dependencies: [
"AdaEngine"
],
path: "Demos/\(path)/",
path: "Demos/",
sources: [
"\(name).swift"
"\(path)/\(name).swift"
],
resources: [
.copy("../Resources/")
.copy("Resources/")
],
packageAccess: false
)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/AdaEngine/AdaEngine/blob/main/LICENSE)
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FAdaEngine%2FAdaEngine%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/AdaEngine/AdaEngine)
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FAdaEngine%2FAdaEngine%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/AdaEngine/AdaEngine)

[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/AdaEngine/AdaEngine)

## What is AdaEngine?

Expand Down
71 changes: 71 additions & 0 deletions Sources/AdaECS/DebugBenchmarkLog.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//
// DebugBenchmarkLog.swift
// AdaECS
//
// Writes NDJSON to a file when CURSOR_DEBUG_LOG_PATH is set (for benchmark profiling).
//

import Foundation

enum DebugBenchmarkLogCounter {
static var entitiesInsertCount = 0
static var insertNewEntityCount = 0
static var moveEntityToArchetypeCount = 0
static var queryEntityLookupCount = 0
static var querySetChunkCount = 0
Comment on lines +11 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Synchronize global benchmark counters

DebugBenchmarkLogCounter stores mutable static Ints that are incremented from multiple runtime paths (Entities.insert, World.insertNewEntity / moveEntityToArchetype, and FilterQueryIterator.next) without any lock/atomic protection. Because these paths can run on different tasks/threads, this introduces a real data race on shared state that can yield undefined behavior (including crashes under race detection) in concurrent ECS workloads. Use ManagedAtomic, an actor, or lock-protected updates for these counters.

Useful? React with 👍 / 👎.

}

public enum DebugBenchmarkLog {

private static let logPath = ProcessInfo.processInfo.environment["CURSOR_DEBUG_LOG_PATH"]
private static let sessionId = "ef35b1"

/// Append one NDJSON line when CURSOR_DEBUG_LOG_PATH is set. No-op otherwise.
public static func write(
location: String,
message: String,
data: [String: Any] = [:],
hypothesisId: String? = nil
) {
guard let path = logPath, !path.isEmpty else { return }
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
var payload: [String: Any] = [
"sessionId": sessionId,
"location": location,
"message": message,
"timestamp": timestamp
]
if !data.isEmpty {
payload["data"] = data
}
if let hid = hypothesisId {
payload["hypothesisId"] = hid
}
guard let json = try? JSONSerialization.data(withJSONObject: payload),
let line = String(data: json, encoding: .utf8)
else { return }
let content = line + "\n"
guard let contentData = content.data(using: .utf8) else { return }
let url = URL(fileURLWithPath: path)
if !FileManager.default.fileExists(atPath: path) {
FileManager.default.createFile(atPath: path, contents: nil)
}
if let handle = try? FileHandle(forWritingTo: url) {
defer { try? handle.close() }
handle.seekToEndOfFile()
handle.write(contentData)
} else {
try? contentData.write(to: url, options: .atomic)
}
}

/// Call from benchmark to mark start/end of a benchmark (when CURSOR_DEBUG_LOG_PATH is set).
public static func benchmarkPhase(name: String, phase: String) {
write(
location: "AdaECSBenchmarks",
message: "benchmark_phase",
data: ["benchmark": name, "phase": phase],
hypothesisId: nil
)
}
}
Loading
Loading