-
Notifications
You must be signed in to change notification settings - Fork 0
Expand documentation, examples, and performance tests #3
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,63 @@ | ||
| # JSimul | ||
| # JSimul | ||
|
|
||
| JSimul 是一个基于 Java 21 的离散事件仿真框架,面向 SimPy 的语义与 API 对齐,提供事件、进程、条件组合以及资源调度等核心能力。项目强调组合优先的设计,通过小而精的原语拼装出丰富的行为,方便在 JVM 环境中重现 SimPy 的建模体验。 | ||
|
|
||
| ## 功能概览 | ||
| - **事件驱动核心**:提供 `Event`、`Timeout`、`Process` 以及 `AllOf`、`AnyOf` 等组合原语,实现基于调度队列的时间推进机制。 | ||
| - **资源建模**:内置 `Resource`、`PriorityResource`、`PreemptiveResource`、`Container`、`Store` 等资源类型,支持优先级、抢占、容量和过滤存取。 | ||
| - **条件与回调**:支持条件事件求值、回调移除与复制等 SimPy 兼容特性,确保回调触发顺序与短路逻辑正确。 | ||
| - **实时模式**:提供 `RealtimeEnvironment`,在需要时可按真实时间步进模拟。 | ||
| - **异常与中断**:支持进程中断、异常传播、主动退出(`env.exit(...)`)等语义,贴合 Python 端行为。 | ||
|
|
||
| ## 性能特点 | ||
| - **优先队列调度**:使用 `PriorityQueue` 与递增事件标识减少争用,确保大批量事件插入与弹出时的可预测性能。 | ||
| - **对象复用友好**:事件与组合条件采用组合式拆分,避免深层继承开销,便于 JVM JIT 进行内联与逃逸分析。 | ||
| - **并发安全的调度器锁**:通过轻量级的队列锁保护调度操作,保证在多生产者场景下的线程安全。 | ||
| - **性能测试护栏**:仓库内的性能单测覆盖大批量超时事件与链式回调场景,帮助在回归时监测吞吐退化。 | ||
|
|
||
| ## 设计美学与哲学 | ||
| - **组合优先**:倾向组合而非继承的实现方式,减少层级,强调可重用的行为片段。 | ||
| - **语义一致性**:以 SimPy 的用户体验为基准,对事件语义、资源 API 和异常传播路径保持一致,降低迁移成本。 | ||
| - **可测试性**:所有核心原语均配有针对功能与 determinism 的测试,新增特性也需要覆盖全面的单元测试。 | ||
| - **简洁编码**:统一使用 UTF-8 编码与英文注释,减少本地化差异带来的理解成本。 | ||
|
|
||
| ## 模块与目录 | ||
| - **`sim` 模块**:核心实现与测试集。 | ||
| - `src/main/java/com/jsimul/core`:事件、进程、环境、条件等核心调度与组合逻辑。 | ||
| - `src/main/java/com/jsimul/collections`:资源、容器、商店等集合型原语。 | ||
| - `src/test/java`:与 SimPy 语义对齐的行为测试、异常传播测试、资源特性测试,以及性能验证用例。 | ||
| - **`examples` 目录**:独立的示例代码,展示从“Hello World”到资源抢占、条件组合的常见建模套路,可直接以 `javac`/`java` 或 IDE 运行。 | ||
|
|
||
| ## 快速开始 | ||
| 1. 安装 JDK 21 与 Maven 3.9+。 | ||
| 2. 构建与运行测试: | ||
| ```bash | ||
| mvn -pl sim test | ||
| ``` | ||
| 3. 浏览 `examples` 目录下的示例,按需编译运行: | ||
| ```bash | ||
| javac -cp sim/target/classes examples/basic/HelloEnvironment.java | ||
| java -cp sim/target/classes:examples basic.HelloEnvironment | ||
| ``` | ||
|
|
||
| ## 特性清单(与 SimPy 对齐) | ||
| - 事件调度:延时、任意值触发、回调管理、状态检查。 | ||
| - 进程语义:生成器式进程、主动退出、异常传递与堆栈保留。 | ||
| - 条件组合:`allOf` / `anyOf` 组合、值合并、失败短路与结果透传。 | ||
| - 资源系统:标准资源、优先级资源、可抢占资源、存储(过滤/优先)、容器容量约束与超时语义。 | ||
| - 运行模式:离散事件模拟与实时模拟切换。 | ||
|
|
||
| ## 示例导航 | ||
| - `examples/basic/HelloEnvironment.java`:最小事件调度与运行示例。 | ||
| - `examples/resources/PriorityCheckout.java`:演示 `PriorityResource` 的占用与释放流程。 | ||
| - `examples/conditions/ConditionalAssembly.java`:组合条件触发与结果收集示例。 | ||
|
|
||
| ## 测试与性能 | ||
| - 功能测试:`sim/src/test/java` 覆盖环境调度、条件组合、资源抢占、异常传播等。 | ||
| - 性能测试:`PerformanceSimulationTest` 验证大批量超时事件与链式回调的执行时间,防止明显性能回退。 | ||
|
|
||
| ## 贡献指南 | ||
| - 提交前确保 `mvn -pl sim test` 通过。 | ||
| - 新增代码需配套单元测试与必要文档更新。 | ||
| - 遵循组合优先的实现方式,避免不必要的继承层级。 | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package basic; | ||
|
|
||
| import com.jsimul.core.Environment; | ||
| import com.jsimul.core.Timeout; | ||
|
|
||
| /** | ||
| * Minimal end-to-end simulation example: schedule two timeouts and run until all | ||
| * events finish. | ||
| */ | ||
| public class HelloEnvironment { | ||
|
|
||
| public static void main(String[] args) { | ||
| Environment env = new Environment(); | ||
|
|
||
| Timeout first = env.timeout(2, "first"); | ||
| Timeout second = env.timeout(1, "second"); | ||
|
|
||
| first.addCallback(event -> System.out.printf("t=%.0f -> %s%n", env.now(), event.value())); | ||
| second.addCallback(event -> System.out.printf("t=%.0f -> %s%n", env.now(), event.value())); | ||
|
|
||
| env.run(); | ||
| } | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package conditions; | ||
|
|
||
| import com.jsimul.core.AllOf; | ||
| import com.jsimul.core.AnyOf; | ||
| import com.jsimul.core.Environment; | ||
| import com.jsimul.core.Process; | ||
| import com.jsimul.core.SimEvent; | ||
| import com.jsimul.core.Timeout; | ||
|
|
||
| /** | ||
| * Illustrates condition composition: wait for any supplier, then for all | ||
| * finishing tasks before shipping. | ||
| */ | ||
| public class ConditionalAssembly { | ||
|
|
||
| public static void main(String[] args) { | ||
| Environment env = new Environment(); | ||
|
|
||
| SimEvent shipment = env.process(shipping(env)); | ||
| AnyOf supplierArrival = env.anyOf(env.timeout(3, "supplier-A"), env.timeout(5, "supplier-B")); | ||
|
|
||
| supplierArrival.addCallback(event -> { | ||
| System.out.printf("t=%.0f -> first delivery: %s%n", env.now(), event.value()); | ||
| env.process(assembly(env)); | ||
| }); | ||
|
|
||
| shipment.addCallback(event -> System.out.printf("t=%.0f -> shipment ready%n", env.now())); | ||
| env.run(); | ||
| } | ||
|
|
||
| private static Process.ProcessFunction assembly(Environment env) { | ||
| return process -> { | ||
| Timeout frame = env.timeout(4, "frame"); | ||
| Timeout electronics = env.timeout(2, "electronics"); | ||
| AllOf finished = env.allOf(frame, electronics); | ||
| yield finished; | ||
| System.out.printf("t=%.0f -> assembly complete%n", env.now()); | ||
| }; | ||
| } | ||
|
|
||
| private static Process.ProcessFunction shipping(Environment env) { | ||
| return process -> { | ||
| yield env.timeout(10); | ||
| System.out.printf("t=%.0f -> shipping triggered%n", env.now()); | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package resources; | ||
|
|
||
| import com.jsimul.collections.PriorityRequest; | ||
| import com.jsimul.collections.PriorityResource; | ||
| import com.jsimul.core.Environment; | ||
| import com.jsimul.core.Process; | ||
|
|
||
| /** | ||
| * Demonstrates how priority alters queue ordering for a resource checkout | ||
| * scenario. | ||
| */ | ||
| public class PriorityCheckout { | ||
|
|
||
| public static void main(String[] args) { | ||
| Environment env = new Environment(); | ||
| PriorityResource cashier = new PriorityResource(env, 1); | ||
|
|
||
| env.process(customer(env, cashier, "regular", 5, 10)); | ||
| env.process(customer(env, cashier, "vip", 2, 4)); | ||
| env.process(customer(env, cashier, "walk-in", 3, 6)); | ||
|
|
||
| env.run(); | ||
| } | ||
|
|
||
| private static Process.ProcessFunction customer(Environment env, PriorityResource cashier, String name, | ||
| int priority, double serviceTime) { | ||
| return process -> { | ||
| PriorityRequest request = cashier.request(priority); | ||
| yield request; | ||
|
|
||
| System.out.printf("t=%.0f -> %s got service\n", env.now(), name); | ||
| yield env.timeout(serviceTime); | ||
|
Comment on lines
+28
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This example uses bare Useful? React with 👍 / 👎. |
||
|
|
||
| cashier.release(request); | ||
| System.out.printf("t=%.0f -> %s finished\n", env.now(), name); | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package com.jsimul.core; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; | ||
|
|
||
| import java.time.Duration; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| /** | ||
| * Performance-oriented tests to guard against major throughput regressions | ||
| * while staying deterministic. | ||
| */ | ||
| public class PerformanceSimulationTest { | ||
|
|
||
| @Test | ||
| void timeoutBurstCompletesWithinBudget() { | ||
| Environment env = new Environment(); | ||
|
|
||
| assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { | ||
| for (int i = 0; i < 20000; i++) { | ||
| env.timeout(1); | ||
| } | ||
|
|
||
| env.run(); | ||
|
|
||
| assertEquals(1.0, env.now()); | ||
| assertEquals(0, env.scheduledCount()); | ||
| }); | ||
| } | ||
|
|
||
| @Test | ||
| void denseCallbackChainExecutesQuickly() { | ||
| Environment env = new Environment(); | ||
|
|
||
| assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { | ||
| Event root = env.event(); | ||
| AtomicInteger counter = new AtomicInteger(); | ||
|
|
||
| for (int i = 0; i < 5000; i++) { | ||
| root.addCallback(evt -> counter.incrementAndGet()); | ||
| } | ||
|
|
||
| root.succeed("ok"); | ||
| env.run(); | ||
|
|
||
| assertEquals(5000, counter.get()); | ||
| assertEquals("ok", root.value()); | ||
| }); | ||
| } | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| package com.jsimul.core; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.*; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| /** | ||
| * Additional parity checks derived from SimPy's event unit tests to | ||
| * validate callback handling, trigger propagation, and empty condition | ||
| * semantics. | ||
| */ | ||
| class SimPyEventParityTest { | ||
|
|
||
| @Test | ||
| void removingCallbacksPreventsInvocation() { | ||
| Environment env = new Environment(); | ||
| Event event = env.event(); | ||
|
|
||
| StringBuilder calls = new StringBuilder(); | ||
| Event.Callback keep = e -> calls.append("keep"); | ||
| Event.Callback remove = e -> calls.append("remove"); | ||
|
|
||
| event.addCallback(keep); | ||
| event.addCallback(remove); | ||
| event.removeCallback(remove); | ||
|
|
||
| event.succeed("done"); | ||
| env.step(); | ||
|
|
||
| assertEquals("keep", calls.toString(), "Removed callback must not run"); | ||
| } | ||
|
|
||
| @Test | ||
| void callbacksAddedAfterSchedulingStillRun() { | ||
| Environment env = new Environment(); | ||
| Event event = env.event(); | ||
|
|
||
| StringBuilder calls = new StringBuilder(); | ||
| event.succeed("payload"); | ||
| event.addCallback(e -> calls.append(e.value())); | ||
|
|
||
| env.step(); | ||
|
|
||
| assertEquals("payload", calls.toString(), "Late-added callback should still fire"); | ||
| assertTrue(event.isProcessed(), "Event should be processed after step"); | ||
| } | ||
|
|
||
| @Test | ||
| void callbacksIgnoredAfterProcessing() { | ||
| Environment env = new Environment(); | ||
| Event event = env.event().succeed("done"); | ||
| env.step(); | ||
|
|
||
| StringBuilder calls = new StringBuilder(); | ||
| event.addCallback(e -> calls.append("late")); | ||
|
|
||
| // Run another harmless step to prove the callback will never execute | ||
| env.schedule(env.event().markOk(null), Event.NORMAL, 0.0); | ||
| env.step(); | ||
|
|
||
| assertEquals("", calls.toString(), "Callbacks added post-processing must be ignored"); | ||
| } | ||
|
|
||
| @Test | ||
| void triggerCopiesOutcomeFromOriginEvent() { | ||
| Environment env = new Environment(); | ||
| Event origin = env.event().succeed("source"); | ||
| env.step(); | ||
|
|
||
| Event target = env.event(); | ||
| target.trigger(origin); | ||
|
|
||
| env.step(); | ||
|
|
||
| assertTrue(target.ok(), "Target should mirror origin success"); | ||
| assertEquals("source", target.value(), "Target value should copy origin"); | ||
| } | ||
|
|
||
| @Test | ||
| void emptyConditionsCompleteImmediatelyWithEmptyValue() { | ||
| Environment env = new Environment(); | ||
|
|
||
| SimEvent all = env.allOf(); | ||
| SimEvent any = env.anyOf(); | ||
|
|
||
| env.step(); | ||
| env.step(); | ||
|
|
||
| ConditionValue allValue = (ConditionValue) all.asEvent().value(); | ||
| ConditionValue anyValue = (ConditionValue) any.asEvent().value(); | ||
|
|
||
| assertTrue(all.asEvent().ok()); | ||
| assertTrue(any.asEvent().ok()); | ||
| assertTrue(allValue.toMap().isEmpty(), "AllOf([]) should carry empty mapping"); | ||
| assertTrue(anyValue.toMap().isEmpty(), "AnyOf([]) should carry empty mapping"); | ||
| } | ||
|
|
||
| @Test | ||
| void runReturnsValueWhenUntilAlreadyProcessed() { | ||
| Environment env = new Environment(); | ||
| Timeout event = env.timeout(1.0, "result"); | ||
|
|
||
| env.run(); | ||
| Object value = env.run(event.asEvent()); | ||
|
|
||
| assertEquals("result", value, "run(until) should return processed event value"); | ||
| assertEquals(1.0, env.now(), 1e-9); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example process is written with Python-like
yield finished;, butProcess.ProcessFunctionexpects you to block on events via the providedProcessContext(e.g.,ctx.await(...)). Usingyieldhere is neither valid Java syntax nor a call to await an event, so the example will not compile or run; the same issue applies to the lateryield env.timeout(10);in this class.Useful? React with 👍 / 👎.